PERSONAL PORTFOLIO
A portfolio site built to showcase projects and blog posts that I write in Notion. I built a custom UI design with two distinct visual themes, keeping everything fast on edge infrastructure, and poured my personality into it. This post covers the architecture, the tradeoffs behind each decision, and what worked or not.
Here is a screenshot of my Notion CMS for my blog:
During the development of this personal portfolio, I built quite a lot of features. Here are some highlights of what I shipped and improved:
Notion API took ~3s to load. After caching them at the edge, they load in <50ms*.
*p50: 41ms, n=100 in prod
Every UI component built from scratch. Full control over every color, shape, and animation.
Designed a dual theme system. Every component is theme-token driven, making development easy.
THE PHILOSOPHY
"Any sufficiently advanced technology is indistinguishable from magic."
We use technology every day without really thinking about how it works. Most of the time, we just trust that it does. But when I started digging deeper into it, I became more and more interested in how much complexity is hidden underneath. A lot of things are abstracted away so well that, from the outside, it almost feels like magic, the kind portrayed in fantasy movies or games. Even after understanding more of it, that feeling never disappears.
And for that reason, I decided to build this personal portfolio as a way to share the magic behind the scenes. To pull back the curtain and show how all the pieces fit together, from the tech stack to the architecture decisions, and even the mistakes and learnings along the way. My hope is that by sharing this, you can learn from my journey and maybe even be inspired to build your own projects.
The floating button on this website is called VeilButton. It's named that way to work as a button to unveil the magic behind the scenes. That's the philosophy of the duality between the magic and cyber themes on this website.
MAGIC
purple · runes · spell circles
CYBER
cyan · circuits · PCB traces
THE STACK
Here are the technology stacks I used to develop this project. Special shoutout to Claude Code for helping me speed this up significantly. You're still expensive though.
FRONTEND
PRESENTATION
BACKEND
SERVICE
INFRASTRUCTURE
EDGE
You may check the details on each part of the stack in the backend and frontend sections below. I combined the infrastructure and backend layers into one section since they are closely related in this project.
BACKEND
The backend is built entirely on Cloudflare's edge infrastructure. This website is deployed as a serverless worker, and there is another worker that pulls blog content from Notion on a schedule and caches it in Workers KV, so the site never waits on Notion at request time. I also put a basic rate limiter and a WAF at the edge layer to keep bad actors from spamming this site. All of those pieces together make the backend blazingly fast.
The personal portfolio is deployed as a Cloudflare Worker via OpenNext. Blog content flows from Notion into Workers KV via the notion-blog-worker, then fetched by the portfolio worker, and finally served to the visitor.
ARCHITECTURE / personal-portfolio
DATA FLOW
NOTION API
api.notion.com
NOTION BLOG WORKER
api.alvinwilta.com
PERSONAL PORTFOLIO
alvinwilta.com
VISITOR
browser
Blog data source can be switched between Notion API and Worker API via env vars.
The notion-blog-worker runs on a separate Cloudflare Worker. Every hour, a cron job fetches all posts from Notion, resolves tags and child blocks, builds a posts index, and writes everything to Workers KV. Reads are served straight from KV, so Notion is never touched on the read path. All read endpoints require an X-API-Key header, keeping the worker private.
ARCHITECTURE / notion-blog-worker
WRITE PATH
CLOUDFLARE TRIGGER
cron 0 * * * * (hourly)
NOTION API
api.notion.com
notion-blog-worker
cron handler
· resolve tags + resources
· fetch child blocks
· build posts:index
· cleanup stale slugs
WORKERS KV
posts:index, post:{slug}
READ PATH
CLIENT
browser fetch
notion-blog-worker
fetch handler
/blog/posts
/blog/posts/:slug
/blog/tags
WORKERS KV
cacheTtl: 3600s
Both Read and Write paths require an X-API-Key for authentication.
Here are some backend implementation highlights:
EDGE-CACHED BLOG API
Notion's API averages 3065ms per request and has strict rate limits. A Cloudflare Worker syncs all blog content to the edge every hour so page loads never wait on Notion. Response time drops to p50: 41ms (n=100 production requests).
- [✓]
CQRS-lite
write path / read path split
- [✓]
Materialized read model
posts:index · post:{slug}
- [✓]
KV + HTTP cache
cacheTtl · max-age = 3600s
- [✓]
API key auth
X-API-Key on reads
- [✓]
Cron-driven sync
0 * * * * · stale-slug GC
GLOBAL EDGE DELIVERY
Responses come from the edge closest to each visitor. No central server round-trip, no cold start, low latency worldwide.
EDGE RATE LIMITING
Burst traffic gets an HTTP 429 at the edge before reaching the Worker. Real visitors see nothing, origin stays cheap.
BENCHMARK ENDPOINT
Click the live demo to compare Notion against the cached Worker side by side. It fetches both the Notion API and blog API in parallel, up to 5 runs to show real variance.
FRONTEND
The frontend is built with Next.js 15 and Tailwind CSS. Every component is built from scratch, with a theme-aware registry that swaps magic and cyber variants automatically. Blog content is rendered via a custom Notion block renderer with no external dependency.
Here are some frontend implementation highlights:
NOTION CONTENT RENDERER
Built from scratch to render every Notion block type: text, headings, code, callouts, toggles, images, columns, video. No external renderer dependency.
CUSTOM UI LIBRARY
Buttons, cards, icons, and decor built from scratch. Even had to create custom renderer for the spells.
THEME-AWARE COMPONENT REGISTRY
Most of the decorations and components have both a magic and cyber version. One hook call returns the right one for the current theme.
SPELL FORGE
Interactive builder for the site's geometric SVG system. Tweak rings, polygons, rotation, and runes live, then export the result as SVG.
KEY DECISIONS
Every project has decision points where multiple reasonable paths exist. These are the ones that shaped how I built this site, what I picked, why, and the tradeoffs I accepted.
LEARNINGS
Not everything went smoothly. Some decisions I made paid off immediately, others cost me more than expected. These are the honest takeaways from building this.
I already have a worker starter kit that I created a few years ago, so to add notion-blog-worker I only had to implement a simple API handler to connect to Notion.
I built the benchmark endpoint to verify the cache was actually faster. It ended up as a public page that proves the architecture decision.
I underestimated this project because it was a personal project, so I never wrote a feature list or roadmap. Scope ballooned from minor tweaks into optimizations. Site ended up solid, but took far longer than needed.
I changed the theme of this website at least three times before settling on the current one. So each time I changed the theme, I had to rewrite every single component to support the new theme's aesthetic.
Thank you for visiting my personal web portfolio. I hope this post inspires you to build something personal and interesting!
GOT THOUGHTS?
Questions or critique are welcome. Feel free to reach out!