
How I built this blog using Notion as a CMS with Next.js and Claude Code, after years of trying different platforms from WordPress to Sanity.
The blog you're looking at right now is built on a Notion database! I bought the tudy.club domain and let it sit for like... n years. I always wanted a place to document the things I like, things I've learned, and things worth recording. So I spent a long time looking for something that would let me shape and display my data exactly the way I wanted.
I tried both, and I'm still casually running AdPost on my Naver blog. It's convenient, for sure. But there were things that bugged me.
Naver Blog is great for showing up in Naver and Google search results, but that's also its downside. Your data is locked into the Naver ecosystem. I wanted to cover dev-related content too, and I needed more flexibility with code blocks and data visualization. The embed limitations and photo uploads were annoying. The Naver editor just wasn't cutting it.
Tistory was the same story. You're still on someone else's platform, and it never really felt like I owned my data. Velog improved a lot in that regard, but what I wanted most was comfortable control over my own data.
Honestly, this desire to own my data goes way back. I set up something similar with WordPress back in high school. But I just... stopped visiting it. Set it up and abandoned it.
That pattern kept repeating. Build it, don't use it, abandon it. To break this loop, the actual writing process had to be as frictionless as possible.
I've been using Webflow and Framer well since 2022, and I even set up previous companies' websites with them, so I originally planned to go that route. But paying monthly felt wasteful. I've been dabbling in coding since 2020, and I already knew how to build things. Why pay extra just for a custom domain and manually upload data? Using a no-code builder felt unnecessary.
So the first thing I tried for building it myself was Sanity. In January 2023, I built a portfolio site using Sanity as a CMS. Sanity is a headless CMS that feels similar to Notion. The flexibility is genuinely great. But the problem was that designing the data schema and wiring everything up at the beginning was really, really painful.
Back in 2023, AI wasn't as capable as it is now, so I had to do all of that manually lol.
I managed to build it somehow, but looking back, I spent more time building the website than focusing on the actual portfolio. My TypeScript skills improved a lot, I'll give it that... but that's not really the point of a blog, right? lol
After going around in circles, I settled on Notion. I'd been using Notion daily since 2020. Writing there, managing projects there. So why not write blog posts in Notion too and have them automatically show up on the site?
Write in Notion, change the status to Published, redeploy, done. Data processing is flexible, and I can naturally use code blocks and tables that dev content needs right inside Notion.
I didn't build everything from scratch. There's an open-source Notion blog template called morethan-log. It's a Next.js static blog that uses a Notion database as a CMS. Clean Vercel deployment support too.
I borrowed a similar Notion DB structure from there, but customized the design and features to match what I wanted.
Head to notion.so/my-integrations and create a new Integration. Give it a name, and you'll get an API token in the format secret_xxxxx. Put this in your NOTION_TOKEN environment variable.
Create a Notion database to manage your blog posts, then go to the page's top-right menu (···) > Connections > add the Integration you just created. If you skip this step, the API won't be able to access the database.
Extract the ID from the database URL and put it in the NOTION_DATABASE_ID environment variable.
https://notion.so/myworkspace/a1b2c3d4e5f6...?v=...
^^^^^^^^^^^^^^^^
This part is the DATABASE_ID
Here are the properties I use:
| Property | Type | Purpose |
|---|---|---|
| title | Title | Post title |
| slug | Text | URL path (e.g., my-first-post) |
| status | Select | Public / Draft |
| type | Select | Post / Resource |
| category | Multi-select | AI, Dev, Design, etc. |
| date | Date | Publish date |
| tags | Multi-select | Tags |
| summary | Text | Preview description (auto-extracted from body if empty) |
| thumbnail | Files | Cover image |
Only items with status Public get fetched by the API, so just switching the status in Notion toggles publish/unpublish.
import { Client } from "@notionhq/client";
const notion = new Client({ auth: process.env.NOTION_TOKEN });
const response = await notion.databases.query({
database_id: process.env.NOTION_DATABASE_ID,
filter: {
property: "status",
select: { equals: "Public" },
},
sorts: [{ property: "date", direction: "descending" }],
});
@notionhq/client is the official SDK, and notion-to-md converts block data to markdown. Notion's block structure is pretty complex, but this library handles most of it.
PublicThat's it. Since Notion serves as the CMS, there's no need for a separate admin page.
Tudy Club is a name derived from (s)tudy club. I designed it so the "s" overlap could be used like products.tudy.club.
The concept behind tudy.club is a subway map. Categories are split into AI, Crypto, Design, Dev, Money, Notion, and Product, and I thought of them as subway lines. Each category is a line, and each post is a station.
When you open a post detail page, you'll see a ticket-style header. Like the ticket you'd get when arriving at a station.
Just like subway lines intersect, learning expands across categories too. You're studying AI and suddenly you're in Dev territory. You dig into Product and it overlaps with Design. Transfer stations where multiple lines meet start to emerge. I imagined it growing and branching out like that.
The purpose of this blog is to log what I learn as I go and spread it out from there. That's why I gave it the subtitle "Growth log of a Gen-Z PM who wants to live better." It's not about delivering finished knowledge -- it's about documenting the process of learning.
I built this blog with help from Claude Code. The job was to reference morethan-log's DB structure while overhauling the design and features to match my concept.
Initial setup and structuring
I started from morethan-log but handed the task of transforming it into my desired structure to Claude Code. From swapping the styling stack to Tailwind + shadcn/ui to organizing the routing structure.
With Sanity, this kind of work took weeks. With Claude Code, it was done in hours.
Notion block rendering
The block data from the Notion API comes in JSON, so you have to build your own renderer. Creating components for each block type -- text, headings, lists, code blocks, images, callouts, etc. -- Claude Code is genuinely fast at this kind of repetitive implementation.
The most annoying problem when building a blog with the Notion API is image expiration. Image URLs hosted by Notion have query parameters like X-Amz-Expires, and the links die after about an hour.
At first I considered shortening the ISR revalidate interval, but I decided caching images locally at build time was cleaner.
// Hash the URL excluding expiration query parameters
const urlObj = new URL(url);
const baseUrl = `${urlObj.origin}${urlObj.pathname}`;
return crypto.createHash('md5').update(baseUrl).digest('hex');
The key is hashing the base URL without query parameters instead of the full URL. Even if the same image comes in with a different expiration time, you get the same hash, so if it's already cached, there's no need to re-download.
At build time, images get saved to public/cached-images/, and the frontend accesses them at /cached-images/{hash}.jpg. When deployed to Vercel, these images land on the CDN with no more expiration worries.
When you add a URL as a bookmark in Notion, it shows up as a nice card. But through the API, you just get the plain URL text. No OG metadata (title, description, thumbnail).
So I built a custom transformer for the notion-to-md library. When it encounters a bookmark block, it fetches the URL's OG data via the Microlink API and renders it as a rich card.
n2m.setCustomTransformer("bookmark", async (block: any) => {
const url = block.bookmark?.url || "";
// Fetch OG metadata via Microlink
const res = await fetch(`https://api.microlink.io?url=${encodeURIComponent(url)}`);
const data = await res.json();
// Cache thumbnail to prevent expiration
const cachedImage = data.data.image?.url
? await cacheImage(data.data.image.url)
: '';
return `🔗BOOKMARK${JSON.stringify({ url, title, description, image, domain })}`;
});
The Microlink API free tier is more than enough, and the fetched thumbnail images are saved by reusing the caching system I built above. Just paste a link in Notion and it shows up as a card on the blog.
Custom domain on Framer costs $5/month ($60/year). Webflow is $14/month ($168/year). Both go up even more if you use CMS features.
My current stack:
Annual savings: $48 to $156
Honestly, it wasn't really about saving money. It was more like: I already know how to build things, so why lock myself into a no-code builder? Claude Code also significantly lowered the barrier to building it myself.
Here's how the writing process changed:
Naver Blog locks in your data, Tistory is the same deal, Framer feels like a waste of money, WordPress was set up in high school and abandoned, Sanity only improved my TypeScript skills. After going around in circles, the Notion + Next.js + Claude Code combo turned out to be the right answer for me.
The key is minimizing the barrier to writing. No matter how cool the setup is, it's meaningless if you don't actually write. Using Notion -- a tool I already use every day -- to write posts and having everything else run automatically was the best decision I made this time.
If you've been sitting on a domain you bought and never used, or if you've been bouncing between platforms like I did, I'd recommend this approach. There are great starting points like morethan-log, and with Claude Code, you can build it in a single weekend day.