Building My Blog with Next.Js and Notion in 2022

🧠 BUILDING MY BLOG WITH NEXT.JS AND NOTION IN 2022

Scale a blog to millions of daily visitors for free

112

Published On Mar 29, 2022 (Updated On Apr 21, 2025) Ā· 1493 Views
Softwarenext.jsreactnotionjavascript

Leave a comment on Notion!

So, I hear you like Next.js and Notion šŸ˜ So do I. I built a scalable production blog in under 12 hours using Next.js with Notion as my CMS.
Ā 
notion image
Ā 
notion image

šŸ‘ŠĀ Motivation

Why Notion?
I have been in love with Notion since July 2019. I initially fell for the simplicity of creating Markdown notes. After three years, my passion for Notion grows with every new feature release. Notion’s block-based data model establishes the base for an incredible information playground, especially if you have a software or data modeling background.
Ā 
In March 2022, Notion released an official API. With this API, we can now programmatically interact with our notion data. This API inspired me to see how far I could take Notion as a CMS for my blog.
Ā 
Here are a few final reasons why I chose Notion as my CMS.
  1. I work in Notion for my personal and professional life every day.
  1. Notion goes beyond markdown to create the best document editing system I have ever used.
  1. With the release of their API, Notion can be used as both a CMS for document content and a simple relational database solution, perfect for small-scale personal projects.
    1. There is no need to spin up a table in Postgres or a collection in Mongo anymore. Create a Notion database with all the columns you need, and you’re good to go. Check out the Advanced Concepts section to learn more.
Ā 
Why Next.js?
In Web 1, we had static HTML. Javascript was written in <script> tags, and websites were presentational and simple.
Ā 
In Web 2, Javascript becomes king. Capability and complexity explode along with page size. Websites are dynamic, contentful, and slow. Speed is exchanged for power.
Ā 
This is where Next.js comes in.
Ā 
Next.js combines the power of Web 2 with the simplicity of Web 1. With Next.js, you can write dynamic and complex React applications that build to simple static files for optimum page performance. Next.js is not the solution for all web applications, but it’s the best solution in 2022 for a simple blog that doesn’t depend on a unique logged-in experience.
Ā 

šŸ’…Ā Content

Now that I’ve convinced you to use Notion and Next.js, here’s what we’ll cover.
Basics
  1. 1ļøāƒ£Ā Create Your CMS Notion Database
  1. šŸ’»Ā Rendering your Notion data in React
Ā 
Advanced
  1. šŸ’½Ā Using Notion as a Relational Database
    1. šŸ“ŽĀ Setting up your notion API integration
    2. šŸ›°ļøĀ Dead-simple like and view features
    3. 🧠 A key-value string cache to store based64 blurred images for beautiful image loading
Ā 

šŸ‘Ā Let’s Get Started

āš–ļøĀ Table of Contents

Ā 

1ļøāƒ£Ā Create Your CMS Notion Database

Our new notion database will host our blog content and a few simple database properties. Duplicate this database template to start. This page makes sure you have all the appropriate properties to power your blog.
Ā 
notion image
Once you have duplicated the blog template as a page in your notion space
  1. Click the share button in the top right.
  1. Click the Share to web button and allow comments.
Why share to the web?
By sharing publicly on the web, readers will be able to comment on your blog. The Notion team plans to add comments to the API by May 2022. Once this is available, we will be able to build native comments into our blog.
notion image
Ā 
notion image

šŸ’½Ā DatabaseĀ Properties

Subtitle, Author, Tags, Categories, Published, Updated, Created → All of these properties are standard blog fields that will help us identify, search and display each blog post.
Upvotes, Views → These two features will act more like traditional relational database columns. We will only interact with them through the notion API. They will count how many upvotes and page views each post gets.
CacheId → This is a reference to another block in Notion. We will fetch this block during build time and edit its children to store a stringified JSON of an image cache.
Ā 
Ā 

šŸ’»Ā Fetching and Rendering Your Notion Content

We will be using the unofficial notion API (i.e., web scraping the same endpoints Notion uses for its clients). Read the below aside to find out why there are no good react renderers for Notion’s official API.
šŸ›£ļøĀ Aside: Why There Are No Good React Renderers for Notion’s Official API (yet)
After Notion moved their API out of Beta, I assumed someone would writeĀ a react renderer for the block objects returned from Notion’s API. Nobody has.
Ā 
Notion’s API is still very immature (as of Mar 28, 2022), and there isn’t support for all block types yet. To get the full range of black content, you need to use the unofficial API. If this is outdated, leave a comment so we can start building react renderers for the official API!
Ā 
Aside-to-the-Aside
I tested rendering blocks from the official notion API using notion-to-md. The results were... not good.
NotionX is already aware of the desire to build a renderer that supports the official Notion API. Follow along with the conversation here. I imagine multiple authors will release rendering libraries once all block types are supported in Notion’s API.
Ā 

šŸ’ÆĀ NotionX

NotionX is a group dedicated to building next-level hacks for Notion. They created a fantastic notion renderer and a simple client to fetch your notion data using Notion’s unofficial API. Follow the steps below to render your first notion page in next.js. This blog post is a simplified introduction. For a more thorough understanding, I recommend reading the docs at NotionX Github.
Install react-notion-x and notion-client
1 npm i --save react-notion-x notion-client
Create a method to fetch a notion page
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // Place this file anywhere in your next project. import { NotionAPI } from 'notion-client'; const api = new NotionAPI({ // This token is your user token. // This is only necessary if you wish to keep your blog database private // It is not needed if your database is public. // See https://www.notion.so/Find-Your-Notion-Token-5da17a8df27a4fb290e9e3b5d9ba89c4 // for how to find your user token. authToken: notionUserTokenV2, }); export function getBlogPost(postId: string) { const post = await api.getPage(postId); return post; }
Fetch your data and render it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // pages/blog.tsx import { NotionRenderer } from 'react-notion-x'; import { ExtendedRecordMap } from 'notion-types'; export async function getStaticProps( context: GetStaticPropsContext ): Promise<GetStaticPropsResult<BlogStaticProps>> { const blogPost = await getBlogPost(postId); return { props: { blogPost: }, }; } export default function BlogPost({ blogPost }: { BlogPost: ExtendedRecordMap }) ( <NotionRenderer recordMap={blogPost} /> );
Ā 

šŸ‘Ā That’s it!

You have everything you need to render notion content in your next.js app. Keep reading to learn about some advanced techniques to power a production blog.
Ā 
Ā 

šŸ”ŒĀ Advanced Concepts

šŸ”‹Ā Using Notion As A Database

Relational databases are no different than a table with rows and columns.
Tables have
rows, and
tables have
columns.
So do databases.
慤
Ā 
That’s why Notion tables are called databases. We can create a relational database using Notion! To interact with Notion as our database, we need to set up an API integration.
Ā 

1ļøāƒ£Ā Create Your Notion Integration

Head to Notion’s integration space and create a new integration.
Make sure to allow reading, updating, and inserting content.
notion image
Ā 
Ā 

2ļøāƒ£Ā Add Your Integration to Your Notion Blog PageĀ 

You need to give your notion integration access to your blog database page.
  1. Click the share button in the top right.
  1. Search for your integration and give it editing privileges.
notion image

3ļøāƒ£Ā Setup Your Next App

šŸ”ØĀ Config
We will add an environment variable for the database ID of our blog. The database ID will be the first URL param if you go to your blog page on a web browser.
1 2 3 4 5 // Example https://www.notion.so/prescottjr/<database_id>?v=fac85496fc5b4cbaa6f315d6b0e4557a // Real https://www.notion.so/prescottjr/a7a4a3733f5445d885fecc257f9e5e80?v=fac85496fc5b4cbaa6f315d6b0e4557a
Ā 
Add this ID to a .env.development file in the root directory of your next project, as shown below.
1 2 NOTION_API_KEY=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx BLOG_DATABASE_ID=databse_id_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Make sure to add these values to your next project’s environment variables, so they are accessible in staging and production environments as well.
Ā 

āŒØļøĀ Interacting With Your Notion Database

šŸ’­
This post will use the Notion javascript SDK. Follow the Notion API documentation for more details.

šŸ…°ļøĀ Pre-Req: Improved Typescript Definitions

Unfortunately, the types bundled with @notionhq/client are challenging to use. They are autogenerated and define unions for every field of every type. Therefore, you have to type narrow before accessing any property on the responses. You can find more info in this Github issue.
Ā 
Because of these ambiguous type definitions, I created a few typescript helpers to make the data returned from Notion’s API more useable. You can find these types here. The most important type to note is NotionPage. NotionPage strongly types all the properties used in the blog database template and narrows types to avoid property may not exist errors on the auto-generated Notion union types.
Ā 
  1. npm i --save @notionhq/client
  1. Query your database in any back end node.js environment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // pages/api/blog/posts/[postId]/like import { NextApiRequest, NextApiResponse } from 'next'; import { Client } from '@notionhq/client'; import { NotionPage, Properties } from '/src/types/cms/properties'; const notionAPIKey = process.env.NOTION_API_KEY; /** * Example next.js API endpoint */ export default async function handler( req: NextApiRequest, res: NextApiResponse ) { const pageId = req.query.postId as string; const notion = new Client({ auth: notionAPIKey, }); const pageData = (await notion.pages.retrieve({ page_id: pageId, })) as NotionPage; res.status(200).json({ pageData }); }
Ā 
Now that you know how to use Notion as a database, we can build some features!
Ā 

ā¤ļøĀ Upvote and View Features

The Upvotes and Views properties are available at blogPost.properties.Upvotes.number or blogPost.properties.Views.number in the NotionPage type and can be used to display in your react application.
Ā 
Once the data is displayed, we can build simple client interactions and API endpoints to update their values.

šŸ‘†Ā Updating A Property

Endpoint to Update Upvotes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // pages/api/blog/posts/[postId]/like import { NextApiRequest, NextApiResponse } from 'next'; import { Client } from '@notionhq/client'; import { NotionPage, Properties } from '/src/types/cms/properties'; const notionAPIKey = process.env.NOTION_API_KEY; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { const pageId = req.query.postId as string; const body = JSON.parse(req.body); const { upvotes } = body; const notion = new Client({ auth: notionAPIKey, }); const pageData = (await notion.pages.retrieve({ page_id: pageId, })) as NotionPage; const currentLikes = (pageData.properties?.Upvotes?.number as number) || 0; const updatedVotes = upvotes < currentLikes + 10 ? Math.max(upvotes, currentLikes + 1) : currentLikes + 1; const updatedPage = (await notion.pages.update({ page_id: pageData.id, properties: { [Properties.Upvotes]: { number: updatedVotes, }, }, })) as NotionPage; res.status(200).json({ pageData: updatedPage }); }
React Callback to Hit Upvote Endpoint
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // BlogPost React Component const [upvotes, setUpvotes] = useState( (pageData?.properties?.Upvotes?.number as number) || 0 ); const [hasUpvoted, setHasUpvoted] = useState(Boolean(false)); const updateLikes = useCallback( throttle(1000, async () => { const currentUpvotes = upvotes; setUpvotes(currentUpvotes + 1); setHasUpvoted(true); localStorage.setItem(getVotedLocalStorageKey(pageData.id), 'true'); fetch(getApiUrl(`blog/posts/${pageData.id}/like`), { method: 'POST', body: JSON.stringify({ upvotes: currentUpvotes + 1 }), }); }), [upvotes] );
Ā 
Ā 

šŸŒ€Ā Image Blurring

This is my favorite feature of the blog. The idea originated from Travis’ tutorial for image caching. Check out his guide for creating base64 image blurs, which relies on the sharp image util. Reference either of these tools to develop base64 strings for blurred images. For example:
Ā 
This image is over 2MB.
notion image
ThisšŸ‘‡Ā is the base64 string to blur the image.
data:image/webp;base64,UklGRlwAAABXRUJQVlA4IFAAAAAQAgCdASoQAAwABUB8JbACdADdsZ4eYyNAAP60Ev/jUvQlDVSKpgCjA6itD0IB9Gggqt674N6Kc8gCLlOimqJskaZ6UH44B6OgXHmMvAAAAA==
Once we have generated our base64 image strings, we want to save them to avoid recomputing them every time our web pages build. Redis is the default answer for a simple string cache. But, if we can use Notion for our database, why can’t we use it as a cache? It will be a slow cache, but we’re not concerned with performance because our pages are statically generated at build time.
šŸ’­
In reality, this cache isn’t necessary. Because of SSG, generating base64 blurred images on every page build would be acceptable. But it is a fun exercise to store non-relational data in Notion 🤷 And it does reduce the likelihood of timeouts when generating your pages. ⚔
Ā 
The Approach
āœ‹
Check out my current image cache page to get context before moving on.
Create a Property on the blog database to store an id reference to another notion block
We’ll use this ID to find another notion block and populate its children with our stringified JSON cache.
notion image
Ā 
Find /create your notion cache → source
āœ‹
We need to break the stringified JSON into multiple notion blocks because the Notion API limits text property updates to 2000 characters. For posts with more than a few images, the cache will exceed 2000 characters.
  1. Find the parent notion block (the cache key).
  1. List all the children of that parent.
  1. Join all the text from the children. This will then be a string representation of a JSON object. JSON.parse it, and now you’ve returned your cache from Notion.
During SSG, create your base64 images → source
If the image is available in the cache, use it. If not, create the base 64 image, save it to an in-memory cache, and return it for the page’s use.
Update the cache in Notion with the in-memory cache → source
Stringify the in-memory object, chunk it into pieces smaller than 2000 characters, and then individually append the chunks as a new child to the parent block.
The next time your page generates, it will use this cache instead of building new images!
Ā 

⚔ Static Page Generation

One of the most notable features of Next.js is the ability to generate pages at build time statically (SSG). I recommend following their docs to learn about SSG.
Ā 
SSG is not required to power your blog, but static generation will be essential to avoid slow page load speeds if you implement the above features.
Ā 

āœ…Ā That’s Actually It!

Thanks for reading. Leave a comment if you have questions or new ideas for fun ways to use Notion!
notion image
Ā 
Ā 
Ā 
Ā 
Ā