DEV Community

Cover image for How I Use Supabase as a Simple CMS for My Portfolio Blog
Ritika Rajput
Ritika Rajput

Posted on Fully Autonomous

How I Use Supabase as a Simple CMS for My Portfolio Blog

When I added a blog to my portfolio, I did not want to edit code and redeploy the site every time I published a post. A small Supabase database solved this for me. In this post I'll show a simple setup you can copy: a table for posts, a storage bucket for images, and a few lines of code to read them.

1. Create a posts table

In the Supabase SQL editor, create a table with the fields a blog card needs:

create table posts (
  id uuid primary key default gen_random_uuid(),
  created_at timestamptz default now(),
  title text not null,
  excerpt text,
  content text,
  date date,
  slug text unique,
  cover_image text
);
Enter fullscreen mode Exit fullscreen mode

The slug is used for the post URL, for example /blog/my-first-post. Making it unique makes sure two posts never share one address.

2. Add a storage bucket for cover images

Open Storage, create a bucket (I named mine site-images) and make it public. Upload an image and copy its public URL. It looks like this:

https://YOUR-PROJECT.supabase.co/storage/v1/object/public/site-images/my-cover.webp
Enter fullscreen mode Exit fullscreen mode

Paste that link into the cover_image column of your post. A tip: compress cover images (WebP at around 1200 x 630 works well) so your blog cards load quickly.

3. Read the posts in your frontend

Install the client and create it with your project URL and anon key:

npm install @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode
import { createClient } from '@supabase/supabase-js'

const supabase = createClient('https://YOUR-PROJECT.supabase.co', 'YOUR-ANON-KEY')
Enter fullscreen mode Exit fullscreen mode

To show the list of blog cards:

const { data: posts, error } = await supabase
  .from('posts')
  .select('title, excerpt, slug, date, cover_image')
  .order('date', { ascending: false })
Enter fullscreen mode Exit fullscreen mode

To load one post on its own page, filter by slug:

const { data: post, error } = await supabase
  .from('posts')
  .select('*')
  .eq('slug', slug)
  .single()
Enter fullscreen mode Exit fullscreen mode

If you write content in Markdown, render it with a library such as react-markdown or marked.

4. Keep it secure with Row Level Security

By default you do not want visitors to change your data. Turn on Row Level Security and allow only reading:

alter table posts enable row level security;

create policy "Public can read posts"
on posts for select
using (true);
Enter fullscreen mode Exit fullscreen mode

With no insert, update or delete policy, visitors can only read. The anon key is meant to be used in the browser, but never put your service_role key in frontend code.

5. Publishing a new post

To publish, I open the Table Editor, insert a new row, paste the title, excerpt, slug and content, and add the cover image link. The post shows up on the site without any code change or redeploy.

Pros and cons

  • Simple and free to start, and no redeploy for new posts.
  • You control your data and can export it any time.
  • The Table Editor is not a writing tool, so long posts feel a bit clumsy. If you publish often, you can later build a small admin page on top of the same table.

Final thoughts

For a personal portfolio, a table, a bucket and a few queries are enough to get a working blog. You can see this kind of setup on the blog section of my portfolio, and you can find more about my work on the Skills page. If you try this, let me know in the comments how it went.

Top comments (0)