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 fromsubscriberswherestatus = 'active'), and pending comments (count fromcommentswherestatus = '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
poststable ordered byviewsdescending - 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
deletePostServer 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
categoriestable - Tags -- multi-select tag picker populated from
tagstable; supports creating new tags inline - Status -- radio group for Draft, Published, or Scheduled
- Scheduled date -- date/time picker shown when status is
scheduled; setsscheduled_at - SEO fields -- meta title and meta description inputs with character count indicators
- Save -- calls
createPostServer 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
getPostServer Action, including category, tags, and author - Pre-filled fields -- all fields populated with existing post data
- Save -- calls
updatePostServer 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_dailytable 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
moderateCommentServer Action to updatestatus - Bulk actions -- select multiple comments for batch approve, reject, or delete
- Delete -- permanently removes the comment via
deleteCommentServer 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
updateAuthorServer Action - Post count -- aggregated from
poststable 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
createTagServer Action - Edit tag -- inline editing of tag name and slug via
updateTagServer Action - Delete tag -- removes tag and all
post_tagsassociations; admin-only viadeleteTagServer 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
updateCategoryServer 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
mediatable, showing filename, dimensions, and file size - Upload -- drag-and-drop or file picker; uploads to the
blog-assetsSupabase Storage bucket viauploadMediaServer Action, then inserts a row into themediatable with the public URL, dimensions, and file size - Alt text editing -- inline editing of alt text for accessibility; calls
updateMediaAltServer Action - Delete -- removes the file from Supabase Storage and the row from the
mediatable; 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
addSubscriberServer Action - Delete -- remove a subscriber via
deleteSubscriberServer 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_atdate - Actions -- edit the post, publish immediately (changes status to
publishedand setspublished_atto 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
updateSiteSettingsServer Action; only accessible to users withadminrole
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
- Public Blog -- public-facing blog routes and interactions
- Server Actions -- complete reference for all data operations
- Authentication -- auth flow and role-based access