AboutProjectsBlog

© 2026 Arman Singh
GitHub
back to blog

Building a Notion-powered blog with Next.js

March 23, 2026

How I built a Notion-powered blog on my Next.js portfolio; setup, rendering, and the bugs I ran into along the way.


Building a Notion-Powered Blog with Next.js

I wanted a blog on my portfolio but didn't want to deal with a heavy CMS or write markdown files manually every time. Notion felt like the right call. I already use it daily, the writing experience is great, and it has an official API.
Here's exactly how I built it, including the bugs I ran into.

Why Notion as a CMS

The alternatives I considered:
  • MDX files — great but means touching the codebase every time I write
  • Contentlayer — good but adds complexity
  • Sanity / Strapi — overkill for a personal blog
Notion wins because I can write a post, check the Published checkbox, and it's live within 60 seconds. No redeploy needed.

Setting Up the Notion Integration

First, create a Notion database with these properties:
  • Title — Title
  • Slug — Text
  • Description — Text
  • Date — Date
  • Published — Checkbox
  • Tags — Multi-select
Then go to notion.so/my-integrations, create a new integration, and copy the API key. Connect it to your database via the Share menu.
Add to .env.local:
NOTION_API_KEY=your_key_here NOTION_BLOG_DATABASE_ID=your_database_id_here

Fetching Posts

Install the official client:
npm install @notionhq/client@2.2.15
Pin to v2 — v3 restructured the API and breaks things.
import { Client } from '@notionhq/client'; const notion = new Client({ auth: process.env.NOTION_API_KEY }); export async function getBlogPosts() { const response = await notion.databases.query({ database_id: process.env.NOTION_BLOG_DATABASE_ID!, filter: { property: 'Published', checkbox: { equals: true } }, sorts: [{ property: 'Date', direction: 'descending' }], }); return response.results.map((page) => { // map properties to a clean BlogPost type }); }

Rendering with react-notion-x

To render Notion blocks exactly as they look in Notion, use react-notion-x:
npm install react-notion-x notion-client prismjs
Since react-notion-x uses createContext, it must run on the client:
'use client'; import { NotionRenderer } from 'react-notion-x'; import dynamic from 'next/dynamic'; const Code = dynamic(() => import('react-notion-x/build/third-party/code').then((m) => m.Code) ); export default function NotionContent({ recordMap }) { return ( <NotionRenderer recordMap={recordMap} fullPage={false} darkMode={true} disableHeader={true} components={{ Collection: () => null, Code }} /> ); }
Important: notion-client uses the unofficial Notion API, so your pages must be set to public in Notion's share settings.

The Linux Casing Bug

This one took me a while. Everything worked perfectly locally on Windows but Vercel kept throwing Module not found errors.
The problem: Windows is case-insensitive, Linux is not. My folders were committed to git as Activities and Contact but my imports used activities and contact.
Fix — rename via a temp folder since Windows won't do direct case renames:
git mv src/components/Activities src/components/activities_temp git commit -m "chore(): temp rename" git mv src/components/activities_temp src/components/activities git commit -m "fix(): lowercase folder names for Linux" git push origin main

Deploying to Vercel

Two things that caught me:
1. Add environment variables — Vercel doesn't read your .env.local. Go to Settings → Environment Variables and add NOTION_API_KEY and NOTION_BLOG_DATABASE_ID manually.
2. Set Framework Preset to Next.js — mine was set to "Other" which caused a 404: NOT_FOUND on every page despite successful builds. Changing it to Next.js fixed it instantly.

Result

Posts are written entirely in Notion. The blog index auto-updates every 60 seconds via revalidate = 60. Code blocks, callouts, toggles — everything renders exactly as it looks in Notion.
Total setup time: a few hours, mostly debugging the casing issue and Vercel config.