Admin CMS

Last updated on 2026-09-03

The admin CMS is a protected area at /admin/* that requires authentication. It provides a full content management system for creating posts, moderating comments, managing media, and viewing analytics. All data comes from Supabase and all mutations use Server Actions.

Server/Client Component Pattern

Every admin page follows the same architecture: a server page.tsx that fetches data via Server Actions and passes it to a *-client.tsx client component for interactivity.

app/admin/posts/page.tsx          --> Server Component (data fetching)
  └── posts-client.tsx            --> Client Component (UI, interactions)
        └── Server Actions        --> Mutations (create, update, delete)
// app/admin/posts/page.tsx (Server Component)
import { getPosts } from "@/lib/actions/posts"
import { PostsClient } from "./posts-client"

export default async function PostsPage() {
  const { data: posts } = await getPosts()
  return <PostsClient initialPosts={posts} />
}
// app/admin/posts/posts-client.tsx (Client Component)
"use client"

import { deletePost } from "@/lib/actions/posts"

export function PostsClient({ initialPosts }: Props) {
  const [isPending, startTransition] = useTransition()
  // Interactive UI with mutations via Server Actions
}

Dashboard

Route: /admin

The admin home page with KPIs, activity feed, and post analytics.

  • KPI cards -- four summary cards showing total published posts, total views (summed from posts.views), active subscribers (count from subscribers where status = 'active'), and pending comments (count from comments where status = 'pending')
  • Activity feed -- recent content activity: new posts published, comments received, new subscribers -- aggregated from multiple tables
  • Post analytics -- top-performing posts by views, fetched from posts table ordered by views descending
  • Quick actions -- buttons to create a new post, moderate comments, and view analytics

Data Sources

KPI Source Query
Total posts posts table count where status = 'published'
Total views posts table sum(views) where status = 'published'
Active subscribers subscribers table count where status = 'active'
Pending comments comments table count where status = 'pending'

Post List

Route: /admin/posts

Post management with filtering, search, and bulk actions.

  • Data table -- columns for title (linked to edit), status badge (draft/published/scheduled), category, author avatar and name, published date, views, and row actions dropdown
  • Status filter -- tabs or dropdown to filter by draft, published, scheduled, or all
  • Search -- text search across post titles using Supabase .ilike("title", query)
  • Author filter -- dropdown to filter posts by author
  • Row actions -- Edit, View on blog (for published), and Delete; delete calls deletePost Server Action with confirmation dialog
  • Create button -- navigates to /admin/posts/new

Post Editor (New)

Route: /admin/posts/new

Full-featured post editor with rich text editing, SEO fields, and category/tag selection.

  • Title -- large text input for the post title; auto-generates slug on blur
  • Slug -- editable URL slug with auto-generation from title
  • Rich text editor -- powered by the Novel editor (Tiptap-based), supporting headings, bold, italic, lists, code blocks, images, links, and embeds
  • Excerpt -- textarea for the post summary shown in listings
  • Featured image -- image upload via Supabase Storage or URL input; preview shown inline
  • Category -- select dropdown populated from categories table
  • Tags -- multi-select tag picker populated from tags table; supports creating new tags inline
  • Status -- radio group for Draft, Published, or Scheduled
  • Scheduled date -- date/time picker shown when status is scheduled; sets scheduled_at
  • SEO fields -- meta title and meta description inputs with character count indicators
  • Save -- calls createPost Server Action; redirects to post list on success

Editor Architecture

PostEditor
├── Title input (auto-slug)
├── Novel rich text editor
│   ├── Toolbar (formatting, headings, lists, media)
│   └── Content area (Tiptap)
├── Sidebar
│   ├── Status selector
│   ├── Category select
│   ├── Tag multi-select
│   ├── Featured image upload
│   └── SEO fields
└── Action bar (Save Draft / Publish)

Post Editor (Edit)

Route: /admin/posts/[id]/edit

Pre-filled post editor with the same UI as the new post editor.

  • Data loading -- Server Component fetches the post by ID with getPost Server Action, including category, tags, and author
  • Pre-filled fields -- all fields populated with existing post data
  • Save -- calls updatePost Server Action; authors can only update their own posts unless they are editors or admins

Analytics

Route: /admin/analytics

Analytics dashboard with pageview trends, post performance, and subscriber growth.

  • Pageview chart -- line chart showing daily views and unique visitors from the pageview_daily table over the last 30 days
  • Post performance -- ranked table of posts by views, with sparklines showing view trends
  • Subscriber growth -- area chart showing cumulative subscriber count over time, derived from subscribers.created_at
  • Date range picker -- filter analytics data by custom date range

Data Sources

Data Source Query
Pageview trends pageview_daily table Ordered by date, filtered by range
Post performance posts table Ordered by views desc, status = 'published'
Subscriber growth subscribers table Grouped by created_at date

Comment Moderation

Route: /admin/comments

Moderation queue for managing user-submitted comments.

  • Tabs -- Pending, Approved, Spam tabs filtering by comments.status
  • Comment cards -- each card shows author name, email, comment content, post title (linked), submission date, and parent comment (if threaded)
  • Moderation actions -- Approve, Reject, and Mark as Spam buttons; each calls moderateComment Server Action to update status
  • Bulk actions -- select multiple comments for batch approve, reject, or delete
  • Delete -- permanently removes the comment via deleteComment Server Action
  • Pending count -- badge in the admin sidebar showing the number of pending comments

Moderation Flow

Public visitor submits comment --> status = 'pending'
                               --> Appears in admin moderation queue
Editor/Admin approves          --> status = 'approved'
                               --> Comment now visible on public blog
Editor/Admin rejects           --> status = 'spam' or deleted

Author Management

Route: /admin/authors

View and manage blog authors and their roles.

  • Author list -- table showing avatar, name, email, role badge, post count, and join date
  • Role display -- colored badges for Admin, Editor, and Author
  • Edit -- admin users can update an author's role via updateAuthor Server Action
  • Post count -- aggregated from posts table per author

Tag Management

Route: /admin/tags

CRUD interface for managing tags.

  • Tag list -- table showing tag name, slug, and post count (aggregated from post_tags)
  • Create tag -- inline form calling createTag Server Action
  • Edit tag -- inline editing of tag name and slug via updateTag Server Action
  • Delete tag -- removes tag and all post_tags associations; admin-only via deleteTag Server Action

Category Management

Route: /admin/categories

CRUD interface for managing categories. Admin-only.

  • Category list -- table showing name, slug, color swatch (oklch), description, image thumbnail, and post count
  • Create category -- form with name, slug, description, color picker, and image upload
  • Edit category -- pre-filled form calling updateCategory Server Action
  • Delete category -- removes category with confirmation; posts in this category are unlinked (set to null)

Media Library

Route: /admin/media

Media management with Supabase Storage integration.

  • Grid view -- thumbnail grid of all uploaded media from the media table, showing filename, dimensions, and file size
  • Upload -- drag-and-drop or file picker; uploads to the blog-assets Supabase Storage bucket via uploadMedia Server Action, then inserts a row into the media table with the public URL, dimensions, and file size
  • Alt text editing -- inline editing of alt text for accessibility; calls updateMediaAlt Server Action
  • Delete -- removes the file from Supabase Storage and the row from the media table; editor-only
  • Copy URL -- click to copy the public URL for use in posts

Upload Flow

User drops file --> uploadMedia Server Action
               --> Upload to Supabase Storage (blog-assets bucket)
               --> Get public URL
               --> Insert row in media table (url, filename, dimensions, file_size)
               --> Return URL for use in post editor

Subscriber Management

Route: /admin/subscribers

Manage newsletter subscribers.

  • Subscriber table -- columns for email, source (where they signed up), status, and subscription date
  • Search -- text search across subscriber emails
  • Growth stats -- total subscribers, new this month, and unsubscribe rate
  • Export -- download subscriber list as CSV
  • Add subscriber -- manually add a subscriber via addSubscriber Server Action
  • Delete -- remove a subscriber via deleteSubscriber Server Action

Scheduled Posts

Route: /admin/scheduled

View and manage posts scheduled for future publication.

  • Scheduled list -- table of posts with status = 'scheduled', showing title, author, category, and scheduled date/time
  • Countdown -- time remaining until each post's scheduled_at date
  • Actions -- edit the post, publish immediately (changes status to published and sets published_at to now), or revert to draft

Site Settings

Route: /admin/settings

Global blog configuration. Admin-only.

  • Blog info -- blog name and description (used in headers, RSS feed, and meta tags)
  • Logo -- logo upload via Supabase Storage
  • SEO defaults -- meta title template and meta description template (used when posts don't have custom SEO fields)
  • Analytics -- Google Analytics measurement ID
  • Save -- calls updateSiteSettings Server Action; only accessible to users with admin role

Settings Data

All settings are stored as a single row in the site_settings table. The getSiteSettings Server Action is called by both public pages (for blog name, logo, meta templates) and the admin settings page.

Next Steps