Back to blog

Hello, world — this blog now exists

Why I wired an MDX blog to my Next.js portfolio, plus the auto-publishing pipeline I bolted on with n8n and Telegram.

4 min read
metanextjsmdx

I've been meaning to add a blog to this site for like a year. Every time I sat down to do it I'd get sidetracked — pick a CMS, get annoyed at it, start over with a different framework, abandon the whole thing. Classic.

So this weekend I just bit the bullet and shipped the dumbest possible version: markdown files in the repo, validated at build time, rendered as static HTML. No database. No CMS. No "headless" anything. If I want to write a post I add a file and push.

#The stack, briefly

Most of the heavy lifting is one library: next-mdx-remote/rsc. It runs as a server component so the post body ships as plain HTML — your browser doesn't download a markdown parser to read my words, which always struck me as ridiculous on the bigger blog setups.

Frontmatter goes through a zod schema with .strict() mode on. If I forget a field or typo tag: instead of tags:, the build fails before deploy. I'd rather have a broken build for an hour than a broken page sitting in production for a week.

const frontmatterSchema = z
  .object({
    title: z.string().min(1).max(120),
    description: z.string().min(1).max(300),
    date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  })
  .strict();

That's it. That's the gate.

Code highlighting is rehype-pretty-code (it wraps Shiki). I picked it specifically because it does the work at build time — the highlighted HTML is in the page source, no client-side syntax-highlight library shipped. Same philosophy as the rest: don't pay runtime cost for stuff a build server can do once.

#The part I actually had fun with

I wired n8n to it.

There's a workflow on a server I run that drafts a post via Groq (llama-3.3-70b-versatile, free tier, fast as anything), formats it as MDX, and sends me an approval card on Telegram with the title, tags, and first ~500 chars of the body. Two buttons: ✅ approve, ❌ reject.

If I tap approve, n8n commits the file to main via the GitHub API. Vercel sees the push and rebuilds. Post is live in like 90 seconds, all from my phone.

It also listens for commands. If I'm out and a topic occurs to me I just send /write something I want to ramble about and the bot drafts it, sends the approval card, same flow.

#Caveats I want to write down before I forget

The LLM still writes like an LLM. I'm probably going to end up rewriting most of what it produces, or treating its output as a rough first draft to react to. That's still better than staring at a blank file at 11pm wondering what to write about.

Auto-publish has a kill switch. If a bad post sneaks through approval (or honestly if I'm half-asleep and tap the wrong button), git revert HEAD && git push rolls it back in about a minute. Boring, reliable, the way I like it.

I might regret the strict frontmatter. A malformed n8n run that somehow gets past my approval would commit a file that breaks the next build, freezing deploys until I fix it. To stop that I run the same Zod-equivalent validation inside the n8n workflow before it commits, so the bad file never gets created in the first place. Belt and suspenders.

#What goes here from now on

Mostly notes on whatever I'm building or breaking. Probably more on Next.js, React Native, AWS, the occasional rant about a tool that wasted my afternoon. Not everything will be polished — that's kind of the point of finally just shipping the thing.

If you want to follow along the feed is right here. And if anything I write turns out to be wrong, tell me — I'd rather know.

Bassam