When I first built bilalh.dev, I intentionally kept the blog system as markdown-in-code.
Not because that’s the “ideal” CMS — but because it’s the fastest way to ship a clean writing setup without turning the project into a full product.
Then came Phase 2 (the plan from day one): a real CMS so I can publish without opening my IDE every time.
This post is how I set it up using v0 by Vercel + Supabase, and how v0 made the process feel… unfairly fast.
The goal
I wanted something simple, but real:
- Admin-only access (no user accounts, no over-engineering)
- A hidden admin URL (not linked anywhere publicly)
- Password login stored in env vars
- Server-side auth (no secrets exposed in the client)
- Articles stored in a database (still markdown)
- A basic editor + rendered markdown preview
What I chose for storage (and why)
I went with Supabase.
Why it’s a great fit for a small portfolio site:
- fast setup
- free tier is usually enough for low-traffic personal sites
- Postgres underneath (simple + powerful)
- easy to deploy + connect to Next.js
Step-by-step: building the CMS with v0
Step 1: Tell v0 exactly what you want (and what you don’t want)
I started by giving v0 a clear spec.
Here’s a real prompt (slightly cleaned up for readability):
> We have markdown-based articles, but they are written in code.
> I want to build a very simple but comprehensive CMS with a proper admin page.
>
> - super light login page for the admin (me)
> - keep the admin URL hidden from all pages (I must type it directly)
> - password stored in env variables
> - login must use server-side fetch to avoid exposing keys to the client
> - I can add articles in markdown format with inputs for slug/title/etc
> - ideally preview the rendered markdown
> - suggest and use a data storage of your liking
I like prompts like this because they:
- lock in constraints early (hidden route, env password, server-side auth)
- leave implementation choices open (storage + structure)
- keep the scope intentionally “small but complete”
Step 2: v0 suggested Supabase and set it up fast
v0 proposed using Supabase for storage, which matched what I wanted.
Then it walked through the connection step, and the flow felt like:
- pick Supabase
- connect project
- generate client utilities
- create a migration SQL file
Step 3: The database table (SQL migration)
v0 generated a SQL script for the articles table and asked me to run it.
This was the exact message:
> Please execute scripts/001_create_articles_table.sql
That’s the part that felt so convenient:
I didn’t have to hand-write a schema from scratch in a separate tool, copy/paste it, and hope I didn’t forget a column.
v0 basically turned it into: “Here’s your migration. Run it.”
A typical schema for this CMS includes fields like:
slug(unique)titleexcerptcontent(markdown)tags(array)cover_imagepublished(boolean)- timestamps
Step 4: Admin login (env password + server-side validation)
Instead of building full auth + users, I wanted the simplest secure setup:
- a single
ADMIN_PASSWORDstored in environment variables - login form submits to a server route/action
- server validates password, then sets an HTTP-only cookie
- cookie gatekeeps the admin pages
> Build the admin login with server-side auth using env password.
> Persist session via HTTP-only cookies.
> No keys in the client.
This approach is perfect for a personal site because:
- you get a real “admin-only” area
- you avoid shipping auth complexity
- it’s still safe enough for the use-case (assuming you use a strong password)
Step 5: Admin UI (CRUD + markdown preview)
After login, v0 generated:
- a dashboard listing articles
- create/edit/delete flows
- an editor with markdown input
- a preview mode that renders markdown
> Make the UI feel lightweight and consistent with the site’s design.
So the CMS didn’t look like a separate “admin tool” — it looks like part of the site.
Step 6: Hook the public blog back to the database
Finally, the blog pages that previously read from content/articles/ were updated to read from Supabase.
That meant:
getAllArticles()became async- pages became server components where needed
- any client components that were calling sync functions had to be adjusted
That’s the real loop with these tools:
1) generate
2) validate
3) fix quickly
4) ship
What the CMS can do now
- ✅ hidden admin URL (not linked anywhere)
- ✅ env password login
- ✅ server-side validation (no secrets on the client)
- ✅ session via HTTP-only cookie
- ✅ create/edit/delete posts
- ✅ markdown editor + preview
- ✅ Supabase DB storage
What I learned
1) Shipping “simple first” was the right move
Markdown-in-code was the correct Phase 1. It helped me start writing immediately.The CMS is Phase 2 — but I only built it once I had momentum.
2) v0 is not “magic” — it’s leverage
You still need to be precise about constraints (auth, hidden route, server-side flow).But once you communicate well, the speed is ridiculous.
3) Supabase is perfect for low-traffic portfolios
If you just want:- posts stored somewhere reliable
- a simple admin editor
- no backend headaches
Next
Now that the CMS is in place, I’ll start posting more articles on bilalh.dev soon — without the friction of “open repo → edit file → commit → deploy” every time.
If you have questions or want to share what you built, feel free to connect with me on LinkedIn or check out my GitHub.