Built from Friction
Software Engineering
The problem I worked around for five years turned out to be my next product.
Pretty much every project I've built in the last five years uses Notion as its CMS. My portfolio, Purl, Quiver, Sanding Space, and a bunch of others that didn't survive long enough to be mentioned here.
I like writing in Notion. What I didn't like was everything I had to do before I could actually use it.
Every new project started with the same routine. Fetch the blocks from the Notion API, handle all the different block types a page might have, and then style each one so it matched the design system of that specific project.
It's not exactly a hard problem. That's probably why I never fixed it properly. I'd just open the last project, copy the rendering code, adapt it to the new one, fix the edge cases I'd forgotten about and restyle everything until it looked right.
I did this for almost five years. At some point it stopped feeling like a problem and just became part of how I start a project.
What finally changed it was Sanding Space1. More than 90% of it is Notion blocks. The layout is the only part that doesn't come from Notion, everything else is content that has to render well.
So this time the rendering wasn't a side detail I could get away with. It was basically the whole product. And setting up Notion for the fifth time, on a project that's literally about caring for small details, felt a bit silly.
I knew what I wanted. Something easy to set up, that supported every Notion block and worked out of the box with the stack I use everywhere: Next.js, Tailwind CSS and shadcn/ui tokens. I didn't want to copy the same code one more time, so I built it.
Notionic is a Notion renderer for React Server Components. You write in Notion and it renders your pages on the server, with no client JavaScript.
Rendering a page looks like this:
import { Client } from "@notionhq/client"import { NotionRenderer } from "notionic"import { fetchBlocks } from "notionic/fetch"
const notion = new Client({ auth: process.env.NOTION_TOKEN })
export default async function Page() { const blocks = await fetchBlocks(notion, "your-page-id") return <NotionRenderer blocks={blocks} />}The part I cared about most is the styling. Notionic uses your Tailwind CSS and shadcn/ui tokens2, so the content follows your theme, dark mode included. It looks like it belongs to your app instead of looking like a Notion page dropped inside it.
You can also swap the component for any block type with your own, or install ready-made ones from the registry3 for things like code blocks, callouts, tables, toggles, tabs, equations and Mermaid diagrams. React is the only runtime dependency.
Today Notionic runs Sanding Space, Purl, Quiver and its own landing page and docs. It also runs my portfolio, so it's rendering this post right now.
And it's not only my projects anymore. Octo Health uses it to run their Stories, their blog about owning and understanding your health over time. Every post there is written in Notion and rendered with Notionic.
Each of these projects has its own design system, and they all render with the same package. Nobody has to copy rendering code from one repo to the next anymore, which was the whole point.
I think a lot of good products are hiding in the things we've learned to tolerate4. The setup you repeat on every project, the code you keep copying from one repo to the next. You stop seeing it because you've already solved it so many times.
If you've built the same thing more than twice, it's probably worth asking whether it should be its own thing.
It took me five years to ask that question. If you use Notion as a CMS too, hopefully Notionic saves you a few of them.
Footnotes
- Sanding Space is where I write short essays about design engineering, each with a live demo: sanding.space.↩
- shadcn/ui themes are CSS variables, so changing your theme changes your Notion content too.↩
- Ready-made shadcn/ui versions of blocks you can edit, listed on the registry page.↩
- Emil Kowalski argues in Friction as a Feature that friction filters out bad ideas; for me it pointed at a good one.↩