
I started this project with a wholesome, innocent goal:
“I’m going to write more.”
Two days later, I had a feed generator, a sitemap pipeline, a custom MDX component system, and an existential crisis about heading slugs.
This is the story of how my blog came to life — and how it ended up being built with TanStack Start, Cloudflare Workers, Content Collections, safe-mdx, and Prose UI.
If you like posts that mix practical engineering, fast feedback loops, and the occasional “why is this taking 10 seconds to reload on an M4 Max?”, you’ll probably enjoy this one.
Starter template (open source): https://github.com/mohamede1945/tanstack-start-blog/
Live demo: https://template.mafifi.dev/ (and mafifi.dev is also a live demo)
Stack: TanStack Start on Cloudflare Workers with Content Collections + safe-mdx + Prose UI
Extras: prerendered static routes, route-derived RSS + sitemap, and Remotion-generated OG images with an incremental manifest
Want updates? RSS is at
/rss.xmland the email subscribe form is at the bottom of this post.
The blog I wanted vs. the blog I ended up building
My non-negotiables were pretty simple:
Write in Markdown/MDX
Deploy to Cloudflare Workers
Great SEO (canonical URLs, Open Graph, meta hygiene)
Auto-generate:
sitemap
RSS
Tailwind + a small set of reusable components (buttons, cards, theme toggle, etc.)
Support for custom pages as MDX (like
/about)A clean writing experience with modern MDX components (callouts, tabs, steps, code groups, etc.)
A newsletter subscription (I’m using Loops right now, but it’s easy to swap)
The fun part: none of this is individually hard.
The hard part is making it all feel simple when you come back to it a week later.
The stack (and why I picked it)
Here’s what the blog supports today:
TanStack Start
Full-stack React framework with file-based routing, SSR, and prerendered static routes.
Cloudflare Workers deployment
Runs as a Worker: fast global edge execution with near-zero operational overhead.
Markdown + MDX (Prose UI)
Rich MDX components for writing: callouts, code blocks, tabs, steps, tables, math — polished by default.
safe-mdx (Cloudflare compatible)
Compiles MDX ahead of time without runtime eval, so Workers don’t choke on “code generation from strings.”
Typed content collections
Discovers MD and MDX files at build time and generates fully typed content APIs.
Tailwind + shadcn/ui
Utility-first styling with a minimal component layer that’s easy to extend.
MDX-driven pages
Static pages like /about are authored as MDX via a dedicated pages collection.
SEO & Open Graph
Canonicals, OG metadata, and route-level SEO without turning the app into meta-tag soup.
OG images via Remotion
Programmatic Open Graph images with a scripted pipeline and an incremental manifest.
Sitemap + RSS generation
Generated at build time from TanStack Start routes so feeds and sitemaps stay accurate.
And yes — you can customize the look and feel by editing styles.css and/or overriding Prose UI’s styling tokens.
Act 1: I started with Astro (because “it’s a blog”)
I began with the official Astro blog template:
npm create astro@latest -- --template blog
It worked… but it looked like a template from an earlier internet era.
So I switched to a more modern Astro starter:
Better. Closer to the vibe I wanted.
Then I did what every developer does when they get 20 minutes of free time:
“Let’s add Tailwind and shadcn components.”
And that’s when I hit my first real problem.
Act 2: The 10-second dev reload that broke my brain
After integrating Tailwind + shadcn-style components (buttons, cards, theme toggle, etc.), Astro — which is normally blazing fast — started taking ~10 seconds to render an empty blog page in development.
Consistently.
On an M4 Max Mac Studio.
That’s… not a “maybe it’s indexing” problem. That’s a “something is fundamentally wrong with my feedback loop” problem.
I build iOS apps, so I’ve paid the Swift/SwiftUI compile tax 😅
Whenever I can speed up the loop, I do.
Lately: ts-go (TypeScript-Go) for absurdly fast type checking.
So a 10-second page reload was simply unacceptable.
To be clear: this might be on me — misconfiguration, plugin mismatch, or a dev-only performance edge case. I’m not here to dunk on Astro. Astro is great.
But I didn’t want to become a part-time web performance detective for a blog.
Act 3: I switched to TanStack Start (because I’ve used it before)
I built https://secallly.com with TanStack, and the development experience was exactly what I wanted:
fast
predictable
type-safe
“boring” in the best way
So I decided: migrate the blog to TanStack Start, deploy to Cloudflare Workers, and rebuild the content pipeline properly.
And this is where my friend Codex enters the story.
Codex got me 70% there (in ~30 minutes)
I gave Codex the patterns I used in SecAlly and asked it to migrate the Astro blog to TanStack Start while preserving functionality.
It wasn’t magic.
It was… shockingly competent scaffolding.
What it got right:
Turned it into a TanStack Start app with a similar structure and UI
Used
gray-matterfor frontmatter parsingUsed
unified+remark+rehypeto render markdown (unstyled HTML at first)Created a
generate-feedsscript for RSS + sitemapGenerated SEO metadata from frontmatter
Rebuilt the component structure it saw in Astro (layouts/head components repeated everywhere)
What it got wrong (or didn’t know):
Didn’t use TanStack layout routes
Content system was too naive (read markdown files on request)
No proper HMR/hot reload for markdown changes
Markdown rendering pipeline was functional but not nice (unstyled, no MDX components)
Still: as a starting point, it was perfect.

Codex got me ~70% of the migration in ~30 minutes.
The real work: turning “it works” into “it’s a platform I can live with”
Once the codebase existed, I started iterating through a list of problems like an engineer with a checklist and too much coffee.
Problem 1: Markdown hot reload (HMR)
The Codex version read markdown files directly when a request came in. That meant:
no proper HMR
no “save file → instantly see changes”
no structured schema for frontmatter
So I switched to Content Collections:
It does the boring stuff extremely well:
organizes content
parses frontmatter with a schema
generates typed output
supports HMR out of the box
This was the first “platform” moment: once content becomes data, everything gets easier.
Here’s the mental model (simplified):
Note: the exact shape of your schema can be different — this is a good starting template, not a hard requirement.
Problem 2: Layouts were duplicated everywhere
The Astro template approach (and Codex faithfully copied it) repeated layout components across routes.
TanStack Start has layout routes for a reason. So I migrated to a clean layout structure:
root layout handles global chrome and CSS
posts routes inherit that layout
the post page focuses on data + rendering
This one change made the codebase easier to reason about immediately.
Problem 2.5: Static pages should be static
TanStack Start supports prerender for routes that don’t need per-request data.
I use it for everything:
the homepage
static pages (like
/about)blog post routes (content is already compiled at build time)
The result: fast first paint, simpler caching, and fewer surprises in production.
Problem 3: Markdown rendering was correct… but ugly
At first, markdown rendering worked — but the output was raw HTML with minimal styling.
I (to be honest, Codex 😁) tried the “classic path”:
highlight.js for code blocks
custom CSS for markdown elements
It helped, but it still didn’t feel like a writing system. It felt like a markdown parser with makeup.
Then Cloudflare reminded me that Workers are not Node.js.
The classic MDX runtime path leans on eval/new Function under the hood, which Cloudflare Workers forbids.
The error looks like this:
In practice, it shows up as:
EvalError: Code generation from strings disallowed for this context
The fix was switching to safe-mdx, which compiles MDX ahead of time and avoids runtime code generation:
Once I moved to safe-mdx, MDX worked on Workers without hacks.
Then I found Prose UI:
It’s not the most famous library, but it’s exactly what I needed:
polished typography
MDX-ready components
server-side Shiki highlighting
consistent component APIs
token-based styling that you can override
Integration was straightforward, especially because Prose UI even has a TanStack Start guide:
The MDX experience I wanted (and what you get)
Once Prose UI was wired up, writing became fun. These are the components I use:
Callout
Card / Cards
Code blocks with titles + line numbers
Code groups (tabbed)
Frame + Image + Caption
Links
Math
Steps
Tables (including tables inside frames)
Tabs
You can see details here:
Callouts
The above code will render the following:
This is a note callout.
No title here — just a compact warning.
Code blocks (with titles + line numbers)
Code groups (tabs + language switching)
Steps (great for “do this, then that”)
The above code will render the following:
Start with a working baseline
Get the site deployed. Even if it’s ugly. Especially if it’s ugly.
Stabilize the content pipeline
Content Collections gives you typed content + HMR.
Make writing enjoyable
Prose UI gives you the “I actually want to write here” feeling.
Adding custom MDX components (my Grid example)
At some point I built my /about page using regular React components. It worked… but it felt wrong.
A personal site should be editable like content, not like a product feature.
So I moved custom pages into MDX too — and the last missing piece was layout: I wanted a top section where text takes 2/3 and an image takes 1/3.
So I made a custom MDX component: Grid + GridCell.
Here’s the exact code:
Then you register it inside src/components/mdx.tsx:
And now you can do this in MDX:
The above code will render the following:
Hi, I'm Mohamed
I build practical systems and obsess over iteration speed.

Styling: Prose UI is opinionated… and that’s a good thing
Prose UI ships with a strong default design system. My site had a slightly different vibe.
The nice part: Prose UI exposes design tokens as CSS variables, so matching your site’s style is mostly “override variables until it looks right.”
The docs are here:
In practice, it looks like:
If you want to go deeper, this is the source of truth for defaults:
The “boring” features that make the blog feel real
A blog feels legit when:
URLs stay stable
posts are discoverable by search engines
social previews look good
RSS exists
sitemap exists
So I built support for:
canonical links
OG meta
optional SEO fields (so you don’t have to fill everything every time)
RSS feed generation
sitemap generation
The sitemap is derived from TanStack Start routes, so the list stays correct as the app grows.
OG images that don’t rot
Open Graph images are easy when you have one page.
They get painful when you have 30 posts and the template changes.
So I used Remotion to generate OG images from code, and a small script pipeline to automate it:
Remotion compositions live in
src/remotionog:socialrenders the default site OG imageog:postrenders a single post OG imageog:all-postsrenders incrementally usingog-metadata.json
The manifest file keeps hashes and a manifestVersion, so when I change the template, I bump the version and only re-render what’s needed.
This is also where I ran into my favorite “works on my machine” moment.
Cloudflare + CI: the TypeScript script that broke only in GitHub integration
Everything deployed fine when I built locally.
Then I enabled Cloudflare’s GitHub integration (CI builds), and it failed.
The reason was beautifully mundane:
Locally I was running my feed generator with Node 24.12.0 — which happily runs TypeScript in my workflow.
Cloudflare’s CI used Node 22.16.0 at the time — and my script invocation assumed more than it should.
The fix was embarrassingly small:
Use Node’s experimental strip-types flag for running TS:
That’s it. Problem solved.
And I got a permanent reminder that CI environments don’t care about my local preferences.
Deploying to Cloudflare Workers (TanStack Start makes this nice)
TanStack Start has official guidance for deploying to Cloudflare Workers:
At a high level, you’ll have:
wrangler.jsonwithnodejs_compatCloudflare Vite plugin configured for SSR environment
a
deployscript that runswrangler deploy
If you’re setting this up from scratch, the docs walk you through it step-by-step.
Newsletter subscriptions (Loops today, easy to swap tomorrow)
I’m using Loops right now (not affiliated). First impression is great:
generous free tier (1,000 free subscribers)
straightforward API integration
domain verification + list management is simple
The integration is basically:
Create an account
Verify your domain
Generate an API key
Create a mailing list
Put the keys in
.env
If you don’t want newsletter subscriptions, you can remove the footer form and call it a day.
If you want a different provider (like Resend), the integration point is conceptually the same:
accept an email
validate
call provider API
show success/error
Costs (this is the part I love)
Here’s what I’m actually paying for this blog:
| Item | Cost | Notes |
Domain (mafifi.dev) | $12.20/year | Bought on Cloudflare. Same domain was ~$20 on GoDaddy when I checked. |
| Cloudflare Workers | $5/month | I’m on the paid plan because I host other sites + use Hyperdrive for database connection pooling. Free tier is very generous. |
| Loops | $0 (so far) | Evaluating. 1,000 free subscribers is a great start. |
What I learned (and why I’m writing this)
This project reminded me of something I already knew, but keep relearning:
Your tools should protect your feedback loop.
When feedback gets slow, you stop iterating.
When you stop iterating, you stop shipping.
And when you stop shipping, you stop writing — which was the whole point.
The “final” system is:
TanStack Start app
Cloudflare Workers deploy
Content Collections powering MDX content + HMR
safe-mdx for Cloudflare-compatible MDX rendering
Prose UI powering typography + MDX components
shadcn-style components for the small UI bits that matter
prerendered static routes for posts + pages
Remotion OG image pipeline with an incremental manifest
RSS + sitemap + SEO handled automatically (sitemap derived from TanStack Start routes)
Loops newsletter integration
This blog is where I’ll mostly write about:
practical iOS engineering
SwiftUI
AI agents
dev tooling
And sometimes: building things outside my comfort zone (like this site).
Want the template?
If you want the exact starter I built from, it’s open source:
There’s a live demo here:
And yes — this blog is another live demo of the same template in the wild.
It’s fully customizable. Ask your AI agent to tweak colors, fonts, spacing, layouts, and components to fit your style.
One last thing: subscribe
If you made it this far, you’re exactly the kind of reader I’m writing for.
I send one email when I publish — no spam, no drip campaigns.
If you like practical engineering and fast feedback loops, subscribe below.
And if you’re the “RSS forever” type: you’re welcome here too.
Appendix: tools that helped me ship faster
I used this a lot during the build:
It sounds small, but anything that closes the feedback loop (search → verify → implement) matters.
Thanks for reading. Now I’m going to do the thing I was supposed to do from the beginning:
write.