Stop 19 · Build Full-Stack Portfolio ProjectsProjects · 12/12

Blogging Platform with CMS

Part of the Full Stack Developer Roadmap

In short

A blog has two faces: a calm, fast reading page for visitors and a busy editor for authors with drafts and previews. Connecting them cleanly is a lovely full stack exercise.

A blog with a content-management interface for authors — posts, drafts, rich text editing, and a public-facing reading experience, all in one full-stack app.

The purple blob carries a finished cake from a messy back kitchen to a clean glass display.

Think of it like this

The bakery's back kitchen

In the back, bakers mix, taste and redo cakes all day. The glass display at the front only shows finished cakes. When one is ready, someone carries it out. Drafts stay in the kitchen; published posts go in the display.

Watch it happen

From draft to the public page

Author
Next.js server
Database
Reader

autosave draft

1/6The editor saves the draft every few seconds. Readers can't see drafts.

The big ideas

What you need to know

Draft vs published

Keep the draft separate from the live content, so editing a published post doesn't change it until you republish.

Sanitise rich text

Rich text editors produce HTML. Clean it on the server before saving, or a post can carry an XSS attack.

Static reading pages

Public posts render as static pages and revalidate on publish. Readers get speed, Google gets HTML.

Author roles

Writers create drafts, editors publish. Check the role in every server action, not just in the UI.

See it in code

Walk through the code

ts
1"use server";2import { revalidatePath } from "next/cache";3import sanitizeHtml from "sanitize-html";4 5export async function publishPost(postId: string) {6  const user = await requireRole("EDITOR");7  const post = await db.post.findUniqueOrThrow({ where: { id: postId } });8  await db.post.update({9    where: { id: postId },10    data: { status: "PUBLISHED", publishedAt: new Date(), html: sanitizeHtml(post.draftHtml), editorId: user.id },11  });12  revalidatePath(`/blog/${post.slug}`);13  revalidatePath("/blog");14}

1/3Only editors may publish. The check runs on the server, so hiding the button isn't the only guard.

A Next.js server action that publishes a post: role check, sanitise, copy draft to live, then refresh the cached public pages.

Save yourself the pain

Common mistakes

Oops

Rendering author HTML with dangerouslySetInnerHTML without cleaning it.

Do this instead

Sanitise on the server when saving, with an allowlist of tags, and keep a strict CSP.

Oops

Rendering every blog post on each request.

Do this instead

Make post pages static and revalidate them when a post is published or updated.

Your mission

First milestone: write, publish, read

~45 min

Quick check

An author edits a published post but hasn't clicked Publish yet. With separate draft and live columns, what do readers see?

Go deeper

  1. 1Build both the public blog and an authenticated CMS for writing/editing posts.
  2. 2Add a draft/published workflow with a rich text or markdown editor.
  3. 3Optimize the public-facing pages for SEO with proper metadata.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge