
Content Management for Personal Sites: Why I Chose Sanity
Building a full CMS for a personal site is a bit overkill. In this post, I share why Sanity is my go-to Headless CMS, plus my setup using an Express.js proxy to keep API tokens completely secure.
Right when I started building this website, I thought about writing articles to share my knowledge and personal experiences with everyone. But building a whole custom CMS just for a portfolio felt like "using a sledgehammer to crack a nut"—it takes way too much time and effort.
So, after hanging around ChatGPT for a bit, I found Sanity. It neatly solved my problem: providing a proper place to manage content without making me break my back coding a whole Admin Panel or worrying about database hosting.
So, what exactly is Sanity?
Headless CMS is a game-changer
Fellow devs are probably no strangers to the concept of a Headless CMS. To put it simply, it "chops off" the display part (Frontend) from the data management part (Backend/Admin).
With Sanity, they provide the storage infrastructure (Cloud) and an open-source tool called Sanity Studio for you to build your own input interface. Your job is just to define the data fields, type the content, and hit save. How you fetch and display that data—whether using Next.js, React, or any other framework—is entirely up to you.
My site, nphuonha.id.vn, currently revolves around 3 main content streams: Projects, News, and static pages like Privacy Policy. Using Sanity is more than enough; it completely eliminates the grunt work of setting up a DB and writing bulky CRUD APIs.
The actual workflow
Playing around with Sanity is pretty chill; the basic flow goes like this:
1. Initialize and host the Studio in a flash Just type exactly one command:
1npm create sanity@latestIt generates a folder containing the source code for the Admin page. Just run it on localhost, and you instantly have your data entry dashboard.
And the best part is: You don't have to drag this entire Admin chunk up to Vercel or a VPS to host it! Once you're done coding the config, just type npx sanity deploy. Sanity will automatically throw this Studio onto their servers and hand you back a link (like project-name.sanity.studio). Effortless, and it saves you a deployment slot on Vercel.
2. UI generated from Code (Schema-driven) This is what I love the most. I don't have to go into a UI and drag-and-drop to create database tables. Whatever fields or data types your input form needs, just define them directly using JavaScript/TypeScript.
For example, my project.ts file looks like this:
1export default {
2 name: 'project',
3 title: 'My Projects',
4 type: 'document',
5 fields: [
6 { name: 'title', title: 'Project Name', type: 'string' },
7 {
8 name: 'slug',
9 title: 'URL Slug',
10 type: 'slug',
11 options: { source: 'title' }
12 },
13 { name: 'mainImage', title: 'Cover Image', type: 'image' }
14 ]
15}
16Save the file, and the Admin UI automatically updates to show a form with text inputs, an auto-generated slug field, and an image upload component. It perfectly fits a developer's mindset.
3. Fetching data and how I "modded" the security flow
Normally, Sanity has a pretty powerful query language called GROQ (Graph-Relational Object Queries). The original scenario is to take the Sanity token and call the API straight from the Frontend to fetch data.
But this is where I had to play my own cards.
Exposing the Sanity token directly on the client side isn't a great security practice (especially when that token has permission to read draft posts). My solution was to sandwich an Express.js server in the middle to act as a proxy.
- The Sanity token is kept hidden securely in a
.envfile on the Express.js server. - Whenever the Frontend needs data, it calls the internal Express API.
- Express holds the token, uses GROQ to ping Sanity for the data, and then returns the JSON to the Frontend.
The Express.js code looks roughly like this:
1app.get('/api/projects', async (req, res) => {
2 try {
3 // Call Sanity from the Backend, keeping the Token safely hidden
4 const projects = await sanityClient.fetch(`*[_type == "project"]{ title, slug, "imageUrl": mainImage.asset->url }`);
5
6 res.json(projects);
7 } catch (error) {
8 res.status(500).json({ error: 'Failed to fetch data from Sanity' });
9 }
10});It takes a little extra time to set up the intermediary backend, but in return, I can sleep soundly knowing the data flow is clear and absolutely secure.
Wrapping up
If you guys want to build a blog, portfolio, or personal website but are too lazy to touch traditional databases/backends, Sanity is truly an option worth trying. Quick setup, highly customizable via code, free cloud hosting for the Admin panel, and a generous Free tier. Pairing it with a proxy backend like Express.js is all it takes to have a smooth and secure system.