Building a Static Blog with Notion as a Headless CMS: Lessons from notion-blog
Hook
Before Notion had an official API, developers were reverse-engineering browser cookies to turn the productivity tool into a makeshift CMS—and it actually worked remarkably well.
Context
In the pre-2021 era of web development, content management systems presented a familiar dilemma: either use a traditional CMS like WordPress with its heavy infrastructure and maintenance burden, or adopt a headless CMS like Contentful that required yet another service subscription and interface for content creators to learn. Meanwhile, many teams were already drafting blog posts, documentation, and articles in Notion—a tool they used daily for notes and collaboration.
The ijjk/notion-blog project emerged as an elegant hack to solve this workflow friction. Why export content from Notion and paste it into a separate CMS when Notion itself has a structured database, rich text editing, and collaboration features? The project demonstrated that with Next.js's Static Site Generation capabilities and some creative API usage, Notion could function as a full-featured content backend. Content creators could write in their familiar environment while developers got the performance benefits of pre-rendered static pages. It was a compelling vision that spawned dozens of similar projects and commercial services.
Technical Insight
The architecture of notion-blog centers on a three-stage pipeline: fetch content from Notion at build time, transform it into React components, and render static HTML pages. The system expects blog posts to live in a Notion table (database) where each row represents a post with properties like slug, published, date, and authors.
The data fetching happens through a custom API client that mimics browser requests to Notion's private endpoints. Here's the core mechanism for retrieving table data:
// Simplified example of the fetching pattern
const NOTION_API = 'https://www.notion.so/api/v3'
async function fetchTableData(pageId: string, token: string) {
const response = await fetch(`${NOTION_API}/loadPageChunk`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cookie': `token_v2=${token}`
},
body: JSON.stringify({
pageId,
limit: 100,
cursor: { stack: [] },
chunkNumber: 0,
verticalColumns: false
})
})
const data = await response.json()
return data.recordMap
}
The token_v2 cookie provides authentication—extracted manually from your browser's developer tools after logging into Notion. This token grants full access to your workspace, which is both the system's greatest convenience and its most significant security vulnerability.
Next.js integration happens through getStaticProps and getStaticPaths for dynamic routing. Each blog post becomes a pre-rendered page:
export async function getStaticPaths() {
const posts = await getBlogIndex()
return {
paths: posts
.filter(post => post.published)
.map(post => `/blog/${post.slug}`),
fallback: false
}
}
export async function getStaticProps({ params }) {
const post = await getPageData(params.slug)
const blocks = await getPageBlocks(post.id)
return {
props: { post, blocks },
revalidate: 10 // Optional ISR
}
}
The clever part is how content structure is handled. In each Notion page, you insert a divider block to separate the preview (everything above) from the full content. The parser identifies this divider and treats it as a content boundary. Block types—paragraphs, headings, images, code snippets—are mapped to React components through a rendering system that walks the block tree.
For deployment, the entire setup is optimized for Vercel (formerly Zeit, where the 'jj' in ijjk worked). You set two environment variables: BLOG_INDEX_ID (the Notion table's page ID found in its URL) and NOTION_TOKEN (your extracted token_v2). On each deployment, Next.js fetches all posts, generates static pages, and serves them as pure HTML with minimal JavaScript for hydration.
The developer experience is remarkably clean once configured. Content editors see only Notion's interface—they toggle the published checkbox when ready to release a post, set the publication date, and the content automatically appears on the next build. No Git commits, no Markdown files, no separate admin panels.
Gotcha
The fundamental limitation is right in the README's bold warning: 'This project is using Notion's private API which can change at any time.' And it has. Notion has adjusted endpoint structures, authentication flows, and response formats multiple times since this project's creation. Each change potentially breaks existing implementations with no deprecation notices or migration paths.
The authentication model creates practical problems beyond API stability. Extracting token_v2 from browser cookies requires opening DevTools, finding the cookie, and copying a long hexadecimal string—not exactly a smooth onboarding experience. Worse, this token provides full access to your entire Notion workspace. If exposed through a misconfigured environment variable or logged in CI/CD output, an attacker gains access to all your company's Notion content. The token also expires periodically, requiring manual refresh.
Content updates reveal another friction point: the static generation model means changes don't appear until you trigger a new build. While Incremental Static Regeneration (ISR) can help, the project's documentation notes that cached pages sometimes require force rebuilds with vc -f even when content changes. This creates a delayed publishing workflow where 'going live' means waiting for CI/CD rather than clicking publish.
Verdict
Use if: You're studying Next.js SSG patterns or building a proof-of-concept integration between Notion and a static site, and you understand this is primarily an educational resource demonstrating what was possible before official APIs existed. The code offers valuable insights into building CMS integrations and handling block-based content rendering. Skip if: You're building anything for production use or need a reliable Notion-to-website solution. Notion's official API (released May 2021) provides stable, documented endpoints with proper OAuth integration and granular permissions. Modern alternatives like react-notion-x or the official Next.js integration examples offer the same workflow benefits without the private API risks. Even for learning purposes, studying implementations built on the official API will teach you more transferable patterns for working with documented third-party services.