Notion as a CMS
- Next.js
- TypeScript
- Notion API
Projects and experience entries live in Notion DBs. No admin panel, no third-party CMS subscription.
I use Notion for most things: tracking work tasks, grocery lists, ranking movies I've seen with my partner, trip planning… and while Notion offers easy-to-share docs and websites, I like to have more control over what the output looks like. While I don't need others to maintain any content here, it's great knowing I can update any entry easily from my phone without the need for new deployments.
How it works
Each project lives as a page inside a Notion database with: a title, a slug (used for the URL), an excerpt… the data you see rendered per project page. There’s more on the Notion side, where I can declare drafts and published pages. Only pages with Status = Published appear here. When you visit /projects, the server fetches the database, filters for published entries, and sorts them newest-first. There's no client-side loading; the HTML arrives with content already in it. Clicking into a project fetches two things in parallel: the page's properties and its content blocks that are mapped to React components and rendered on the server before anything is sent to the browser.
Caching
Every Notion API call uses Next.js's built-in revalidation, set to 60 seconds. When the first request at any given minute hits Notion, subsequent ones get a cached response. When I publish something new or edit an existing entry, it shows up on the site within about a minute — no deployment needed 🎉
Notion block renderer
The richest part of this is translating Notion's block format into HTML. Notion represents page content as a list of typed blocks — each one has a type field and a corresponding shape. The renderer handles:
- Paragraphs, headings (h1–h3), and dividers
- Bulleted and numbered lists
- Blockquotes and callouts
- Images (with captions), via Next.js's <Image> for optimization
- Code blocks with syntax highlighting
- Column layouts — these nest recursively, so the fetcher walks the tree to pull children of children before rendering Text formatting (bold, italic, code, strikethrough, underline, links) is applied by layering wrappers around each text span, matching Notion's own annotations structure.
What lives where
- lib/notion.ts — all API calls, type definitions, and data mapping
- components/notion-renderer/ — the block-to-JSX renderer
- app/[category]/page.tsx — the projects list page
- app/[category]/[slug]/page.tsx — individual project pages
Publishing workflow
- Write the entry in Notion, fill in the properties
- Set Status → Published
- Within 60 seconds, it appears on the site