DEV Community

Cover image for How to Build a Professional Next.js Blog with a Secure CMS
edmondgi
edmondgi

Posted on

How to Build a Professional Next.js Blog with a Secure CMS

Building a blog is often the "Hello World" of web development, but building a production-grade publication platform is a different beast. You need a fast, SEO-friendly public site for readers and a secure, powerful dashboard for your editorial team.

In this tutorial, we will build "TechChronicles," a dual-interface application:

  1. The Public Front: A high-performance Next.js app for readers.
  2. The Admin Console: A restricted area for editors to draft, review, and publish content.

We will focus on the architecture, the data flow, and solving the critical challenge of managing two distinct user types (Subscribers vs. Editors) without building two separate backends.


Phase 1: The Architecture

We want to avoid the "Monolithic WordPress" trap.
We also want to avoid over-engineering microservices.

Our Stack:

  • Frontend: Next.js 14 (App Router)
  • Backend: Node.js + Express (API)
  • Database: PostgreSQL + Prisma ORM
  • Auth: Rugi Auth (Centralized Identity)

The "Dual-Face" Strategy

Instead of mixing logic, we treat the "Public User" and the "Admin User" as different contexts.

  • reader.techchronicles.com -> Optimized for reading, caching, and speed.
  • admin.techchronicles.com -> Optimized for editing, data management, and security.

Phase 2: Project Setup & Data Modeling

Let's start by defining our data. We need to store posts, but we also need to know who wrote them.

1. The Schema

In schema.prisma, we define a Post that links to an authorId.

// prisma/schema.prisma

model Post {
  id        String   @id @default(uuid())
  title     String
  slug      String   @unique
  content   String   // Markdown or HTML
  published Boolean  @default(false)
  authorId  String   // The Rugi Auth User ID
  createdAt DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

2. The API Server

Set up a simple Express server to serve these posts.

// src/server.ts
import express from 'express';
import { prisma } from './db';

const app = express();

// Public Endpoint: Everyone can see published posts
app.get('/api/posts', async (req, res) => {
  const posts = await prisma.post.findMany({
    where: { published: true }
  });
  res.json(posts);
});

// Admin Endpoint: Create a post (Wait, we need to protect this!)
app.post('/api/posts', async (req, res) => {
  // TODO: Verify if the user is actually an editor
  // const post = await prisma.post.create(...)
});
Enter fullscreen mode Exit fullscreen mode

Phase 3: The Authentication Layer

Here is where most tutorials get bogged down. You start implementing "Sign up", "Login", "Forgot Password", "Email Verification", "JWT signing"... and suddenly your blog project is an auth project.

We will skip all that boilerplate by using Rugi Auth.

1. Initialize Rugi Auth

Run the initializer in a separate folder or container alongside your API.

npx rugi-auth init auth-service
Enter fullscreen mode Exit fullscreen mode

2. Configure the "Dual" Apps

This is the secret sauce. We don't just create one "App". We create two distinct logical apps in Rugi Auth that share the same user pool.

  • App 1: TechChronicles Public
    • Goal: Allow readers to subscribe/comment.
    • Type: Public
    • Roles: SUBSCRIBER
  • App 2: TechChronicles Admin
    • Goal: Allow staff to write news.
    • Type: Confidential
    • Roles: EDITOR, ADMIN

3. Integrating the Protection

Now, back to our API. We can secure that POST route.

// src/middleware/auth.ts
import { verifyToken } from './utils'; // Your standard JWT verification

export const requireEditor = async (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  const user = await verifyToken(token);

  // Check if they have the 'EDITOR' role specifically for the Admin App
  const adminRole = user.roles.find(r => 
    r.appId === process.env.ADMIN_APP_ID && 
    r.name === 'EDITOR'
  );

  if (!adminRole) return res.status(403).send("Editors only.");

  req.userId = user.id;
  next();
};
Enter fullscreen mode Exit fullscreen mode

Phase 4: Building the Frontends

The Public Reader Site (Next.js)

This is standard Next.js. We fetch the published posts.

// app/page.tsx
export default async function HomePage() {
  const posts = await fetch('http://api/posts').then(r => r.json());

  return (
    <main>
      <h1>Latest News</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Admin Dashboard (React/Next.js)

This is where Rugi Auth shines. We need a login screen.

Instead of coding form state, error handling, and validation:

  1. Create a "Login" button.
  2. Redirect the user to your Rugi Auth hosted login page: http://localhost:7100/auth/login?client_id=ADMIN_APP_ID

When they successfully log in as an Editor, they are redirected back to your dashboard with a secure token.

// app/admin/dashboard/page.tsx
"use client";

// This page is only accessible if you have a valid token
export default function Dashboard() {
  const { createPost } = useApi();

  return (
    <div>
      <h1>Editor Dashboard</h1>
      <button onClick={() => createPost({ title: "New Article" })}>
        Publish Article
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why This Approach Wins

  1. Separation of Concerns: Your public blog code doesn't know about "admin rights". It just displays content. Your API handles the gatekeeping.
  2. Unified Identity: If a Subscriber is promoted to an Editor, you just add a role in Rugi Auth. You don't need to migrate their account.
  3. Speed: We built a secure, role-protected CMS backend in minutes by offloading the complex user management logic.

Next Steps

  • Add a "Comments" section to the public site, requiring the SUBSCRIBER role.
  • Add an "Admin-Only" route to manage users (which proxies requests to Rugi Auth's API).

Top comments (0)