Skip to main content
RapidDev - Software Development Agency
how-to-build-v030-60 minutes

How to Build Blog backend with V0

Build a headless blog backend with V0 using Next.js and Supabase. You'll get a rich markdown editor, draft/publish workflow, ISR with on-demand revalidation, SEO metadata, and a comment moderation system — all in about 30-60 minutes without any local setup.

What you'll build

  • Blog listing page with ISR and Card-based post previews featuring cover images and category Badges
  • Individual post pages with generateStaticParams for static generation and on-demand revalidation
  • Writing dashboard with markdown editor, draft/publish toggle, and post management Table
  • SEO metadata fields (meta title, description) with generateMetadata for each post
  • Comment system with moderation queue and approval workflow
  • On-demand ISR revalidation triggered by Server Actions when posts are published
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read30-60 minutesV0 FreeApril 2026RapidDev Engineering Team
TL;DR

Build a headless blog backend with V0 using Next.js and Supabase. You'll get a rich markdown editor, draft/publish workflow, ISR with on-demand revalidation, SEO metadata, and a comment moderation system — all in about 30-60 minutes without any local setup.

What you're building

A blog is one of the most effective marketing tools for any business. But most blog platforms are either too simple (no SEO control) or too complex (full CMS overhead). What founders really need is a fast, SEO-optimized blog with a simple writing experience.

V0 generates the entire blog architecture from prompts — the listing page, individual post pages with ISR, the writing dashboard with markdown, and the comment system. Supabase via the Connect panel provides the database for posts and comments, plus Storage for cover images.

The architecture uses Next.js ISR (Incremental Static Regeneration) so blog pages are statically generated for maximum speed and SEO performance. When a post is published, a Server Action calls revalidatePath to regenerate the page instantly. Server Components handle all data fetching, and the markdown editor is a Client Component.

Final result

A production-ready blog with a writing dashboard, markdown editing, draft/publish workflow, ISR-powered public pages with SEO metadata, cover image uploads, and a comment system with moderation.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
Supabase StorageStorage

Prerequisites

  • A V0 account (free tier works for this project)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Basic familiarity with markdown syntax (optional but helpful)

Build steps

1

Set up the project and blog database schema

Create a new V0 project and connect Supabase via the Connect panel. Prompt V0 to create the posts, categories, and comments tables with proper constraints for a blog with draft/publish workflow.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a blog backend with Supabase. Create these tables:
3// 1. posts: id (uuid PK), author_id (uuid FK to auth.users), title (text), slug (text unique), content (text), excerpt (text), cover_image_url (text), status (text CHECK in 'draft','published','archived'), published_at (timestamptz), meta_title (text), meta_description (text), created_at (timestamptz), updated_at (timestamptz)
4// 2. categories: id (uuid PK), name (text), slug (text unique)
5// 3. post_categories: post_id (uuid FK), category_id (uuid FK), PRIMARY KEY (post_id, category_id)
6// 4. comments: id (uuid PK), post_id (uuid FK), author_name (text), content (text), is_approved (boolean default false), created_at (timestamptz)
7// Add RLS: anyone can read published posts, only authors can edit their own posts, comments require approval to be visible.
8// Generate the SQL migration.

Pro tip: Use Design Mode (Option+D) after the blog listing page is generated to visually adjust card spacing, typography, and cover image aspect ratios without spending any credits.

Expected result: Supabase is connected with all blog tables created, RLS policies applied, and the schema ready for content.

2

Build the public blog listing and post pages with ISR

Create the blog listing page that shows published posts as Cards, and individual post pages that are statically generated with ISR. Each post page uses generateMetadata for SEO and generateStaticParams for static generation.

app/blog/[slug]/page.tsx
1import { createClient } from '@supabase/supabase-js'
2import { notFound } from 'next/navigation'
3import type { Metadata } from 'next'
4
5const supabase = createClient(
6 process.env.SUPABASE_URL!,
7 process.env.SUPABASE_SERVICE_ROLE_KEY!
8)
9
10export const revalidate = 3600
11
12export async function generateStaticParams() {
13 const { data: posts } = await supabase
14 .from('posts')
15 .select('slug')
16 .eq('status', 'published')
17 return posts?.map((post) => ({ slug: post.slug })) ?? []
18}
19
20export async function generateMetadata({
21 params,
22}: {
23 params: Promise<{ slug: string }>
24}): Promise<Metadata> {
25 const { slug } = await params
26 const { data: post } = await supabase
27 .from('posts')
28 .select('meta_title, meta_description, cover_image_url')
29 .eq('slug', slug)
30 .eq('status', 'published')
31 .single()
32
33 if (!post) return {}
34 return {
35 title: post.meta_title,
36 description: post.meta_description,
37 openGraph: { images: post.cover_image_url ? [post.cover_image_url] : [] },
38 }
39}
40
41export default async function PostPage({
42 params,
43}: {
44 params: Promise<{ slug: string }>
45}) {
46 const { slug } = await params
47 const { data: post } = await supabase
48 .from('posts')
49 .select('*, comments(id, author_name, content, created_at)')
50 .eq('slug', slug)
51 .eq('status', 'published')
52 .single()
53
54 if (!post) notFound()
55
56 return (
57 <article className="mx-auto max-w-2xl py-8">
58 <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
59 <div className="prose prose-lg" dangerouslySetInnerHTML={{ __html: post.content }} />
60 </article>
61 )
62}

Expected result: Blog posts are statically generated at build time and revalidated every hour. Each post has proper SEO metadata and Open Graph images.

3

Create the writing dashboard with post management

Build the author dashboard with a table of all posts, create/edit forms with a markdown editor, and a draft/publish toggle. The publish action triggers ISR revalidation so the public page updates instantly.

app/actions/posts.ts
1'use server'
2
3import { createClient } from '@supabase/supabase-js'
4import { revalidatePath, revalidateTag } from 'next/cache'
5
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function publishPost(postId: string) {
12 const { data: post, error } = await supabase
13 .from('posts')
14 .update({
15 status: 'published',
16 published_at: new Date().toISOString(),
17 updated_at: new Date().toISOString(),
18 })
19 .eq('id', postId)
20 .select('slug')
21 .single()
22
23 if (error) throw new Error(error.message)
24
25 revalidatePath('/blog')
26 revalidatePath(`/blog/${post.slug}`)
27}
28
29export async function savePost(formData: FormData) {
30 const title = formData.get('title') as string
31 const content = formData.get('content') as string
32 const excerpt = formData.get('excerpt') as string
33 const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
34 const metaTitle = formData.get('meta_title') as string || title
35 const metaDescription = formData.get('meta_description') as string || excerpt
36
37 const { error } = await supabase.from('posts').insert({
38 title,
39 slug,
40 content,
41 excerpt,
42 meta_title: metaTitle,
43 meta_description: metaDescription,
44 status: 'draft',
45 })
46
47 if (error) throw new Error(error.message)
48 revalidatePath('/dashboard/posts')
49}

Pro tip: Use revalidatePath('/blog') in the publish action to regenerate the listing page, and revalidatePath('/blog/' + slug) to regenerate the specific post page. This gives you instant updates without full rebuilds.

Expected result: The dashboard shows all posts in a table with status badges. Publishing a post triggers instant ISR revalidation of both the listing and post pages.

4

Add cover image uploads with Supabase Storage

Enable image uploads for blog post cover images using Supabase Storage. Create a public bucket and generate an upload component that stores images and returns the public URL to save in the post record.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a cover image upload component for the blog post editor.
3// Requirements:
4// - Drag-and-drop area with preview using a 'use client' component
5// - Upload to Supabase Storage 'covers' public bucket
6// - Accept jpg, png, webp up to 5MB
7// - Show upload progress and preview after upload
8// - Return the public URL to store in posts.cover_image_url
9// - Use shadcn/ui Card for the drop zone, Button for manual select
10// - Add a delete button to remove the current cover image
11// - Use createBrowserClient for the upload (client-side operation)

Expected result: The post editor includes a drag-and-drop cover image uploader. Images are stored in Supabase Storage and the public URL is saved with the post.

Complete code

app/actions/posts.ts
1'use server'
2
3import { createClient } from '@supabase/supabase-js'
4import { revalidatePath } from 'next/cache'
5import { redirect } from 'next/navigation'
6
7const supabase = createClient(
8 process.env.SUPABASE_URL!,
9 process.env.SUPABASE_SERVICE_ROLE_KEY!
10)
11
12export async function savePost(formData: FormData) {
13 const title = formData.get('title') as string
14 const content = formData.get('content') as string
15 const excerpt = formData.get('excerpt') as string
16 const coverImageUrl = formData.get('cover_image_url') as string
17 const metaTitle = (formData.get('meta_title') as string) || title
18 const metaDescription = (formData.get('meta_description') as string) || excerpt
19
20 const slug = title
21 .toLowerCase()
22 .replace(/[^a-z0-9]+/g, '-')
23 .replace(/(^-|-$)/g, '')
24
25 const { error } = await supabase.from('posts').insert({
26 title,
27 slug,
28 content,
29 excerpt,
30 cover_image_url: coverImageUrl || null,
31 meta_title: metaTitle,
32 meta_description: metaDescription,
33 status: 'draft',
34 })
35
36 if (error) throw new Error(error.message)
37 revalidatePath('/dashboard/posts')
38 redirect('/dashboard/posts')
39}
40
41export async function publishPost(postId: string) {
42 const { data: post, error } = await supabase
43 .from('posts')
44 .update({
45 status: 'published',
46 published_at: new Date().toISOString(),
47 })
48 .eq('id', postId)
49 .select('slug')
50 .single()
51
52 if (error) throw new Error(error.message)
53 revalidatePath('/blog')
54 revalidatePath(`/blog/${post.slug}`)
55}
56
57export async function unpublishPost(postId: string) {
58 await supabase
59 .from('posts')
60 .update({ status: 'draft' })
61 .eq('id', postId)
62 revalidatePath('/blog')
63 revalidatePath('/dashboard/posts')
64}

Customization ideas

Add a rich text editor

Replace the markdown Textarea with Tiptap or Novel editor for a WYSIWYG editing experience with formatting toolbar, image embedding, and code blocks.

Add RSS feed generation

Create an API route at app/feed.xml/route.ts that generates an RSS 2.0 feed from published posts using Server Components and returns XML with the correct content type.

Add reading time estimation

Calculate reading time from the word count of the post content (average 200 words per minute) and display it as a Badge on both the listing and post pages.

Add newsletter subscription

Add an email capture form at the bottom of blog posts that subscribes readers via the Resend API, stored in a subscribers table in Supabase.

Common pitfalls

Pitfall: Forgetting to call revalidatePath after publishing a post

How to avoid: Call revalidatePath('/blog') and revalidatePath('/blog/' + slug) in the publish Server Action. This instantly regenerates both the listing and the individual post page.

Pitfall: Not adding generateMetadata for SEO on post pages

How to avoid: Export a generateMetadata async function in each dynamic page that fetches the post's meta_title and meta_description from Supabase and returns them as Metadata.

Pitfall: Using dangerouslySetInnerHTML without sanitization

How to avoid: Use a library like react-markdown for rendering markdown safely, or sanitize HTML with DOMPurify before using dangerouslySetInnerHTML.

Best practices

  • Use ISR with revalidatePath for instant content updates without full rebuilds — set a fallback revalidate interval of 3600 seconds
  • Use generateStaticParams for blog posts to pre-render all published posts at build time for maximum SEO performance
  • Use generateMetadata to create unique title, description, and Open Graph tags for each blog post
  • Store cover images in a Supabase Storage public bucket for fast CDN delivery
  • Use Server Components for all public-facing blog pages to keep database queries server-side
  • Use Design Mode (Option+D) to visually refine the blog card layout, typography, and cover image styling without spending credits
  • Add RLS policies so only post authors can edit their own content and public users can only read published posts
  • Auto-generate URL slugs from post titles to ensure clean, SEO-friendly URLs

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a blog backend with Next.js App Router and Supabase. I need ISR with on-demand revalidation, a markdown editor, draft/publish workflow, SEO metadata with generateMetadata, and a comment system. Help me design the ISR strategy with revalidatePath for instant content updates.

Build Prompt

Build an ISR-powered blog listing page that fetches published posts from Supabase, displays them in a responsive Card grid with cover images, excerpts, and category Badges. Set revalidate to 3600 as a fallback. When the publish Server Action fires, it calls revalidatePath('/blog') to regenerate the listing instantly. Each Card links to the individual post page at /blog/[slug].

Frequently asked questions

What is ISR and why should I use it for a blog?

ISR (Incremental Static Regeneration) pre-renders pages at build time and serves them from the CDN edge. When content changes, revalidatePath regenerates only the affected pages. This gives your blog the speed of static sites with the flexibility of dynamic content.

Can I use the V0 free plan to build a blog backend?

Yes. A basic blog requires only a few pages and Server Actions, which fits within the V0 free plan's credit allocation. The blog is one of the simplest projects to build with V0.

Should I use markdown or a rich text editor?

Markdown is simpler and recommended for technical blogs. For non-technical content creators, consider adding Tiptap or Novel editor for WYSIWYG editing. V0 can generate either approach from a prompt.

How do I handle SEO for individual blog posts?

Export a generateMetadata function in your app/blog/[slug]/page.tsx that fetches the post's meta_title and meta_description from Supabase. Next.js automatically generates the correct title tag, meta description, and Open Graph tags.

How do I deploy the blog?

Click Share then Publish to Production in V0 for instant Vercel deployment. Blog pages are statically generated on first request and cached at the edge. Publishing new content triggers on-demand revalidation.

Can I add multiple authors to the blog?

Yes. The posts table includes an author_id foreign key. Add a join to the profiles table when fetching posts to display author names and avatars. RLS policies ensure authors can only edit their own posts.

Can RapidDev help build a custom blog or CMS?

Yes. RapidDev has built 600+ apps including custom content platforms with multi-author workflows, SEO optimization, and newsletter integration. Book a free consultation to discuss your content needs.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.