Admin UX is part of the product
If publishing a blog post or creating a course feels fragile, you will stop updating the site. Customers never see that friction — they only see silence.
So we treat admin forms like production UI: predictable, validated, and fast to navigate.
The Desi Stack pattern
1. Server page.tsx owns loading data
Example: edit blog loads the post by id, 404s if missing, passes serializable initial props into the form.
Server code stays free of useState. It can talk to Prisma directly and keep secrets off the wire.
2. Client form owns interaction
BlogForm, CourseForm, ProjectForm handle:
- Controlled fields
- Auto-slug from title
- Write / preview tabs for Markdown
- Upload vs URL modes for media
- Toasts + disable states while saving
For huge wizards (projects), a Zustand store can hold multi-step state without prop-drilling hell. For blogs, a single page form is enough — do not cargo-cult five steps.
3. API routes validate with Zod
Never trust the browser. The admin API:
- Calls
checkAdminAccess() - Parses the body with Zod
- Normalizes tags/slugs
- Computes derived fields (reading time)
revalidatePathfor public pages after mutate
Errors return clear JSON the form can toast.
4. Media goes through a dedicated upload route
MediaUploadField posts to /api/admin/upload with a folder like blogs. The form stores the returned URL — not a File object in the database.
That keeps Postgres lean and CDN/storage where it belongs.
List pages that do not feel frozen
Admin navigation used to feel blank while heavy server pages awaited. We fix that with:
loading.tsxskeletons per segment- Client tables that filter quickly after initial data
- Avoiding 13 sequential queries on every click when a lighter query will do
Trust is: click → immediate feedback → content.
Draft / publish as a first-class state
Content features need more than “save.”
For blogs we use draft | published | archived:
- Drafts never hit public routes
- Publish sets
publishedAton first publish - Archive hides without deleting history
- Delete is confirm-modal only
Courses/projects can mirror the same mindset with status fields and explicit buttons.
Checklist for a new admin resource
- Prisma model + migration
GET/POSTlist+create admin API,GET/PUT/DELETEby id/admin/<resource>table +loading.tsxnew+[id]/editpages with shared form component- Sidebar link
- Public routes if the resource is public-facing
- Seed script data living under
src/data/
If any of those are missing, you will feel it during the second week of use.
What we refuse to overbuild in v1
- Block editors when Markdown is enough
- Soft realtime collaboration in admin
- Ten-step wizards for three fields
- Client-only “security”
Ship the boring CRUD that lets you publish daily. Decorate later.
See it live
Create or edit posts at /admin/blogs. The public side is /blog. Same pattern powers courses and projects across the panel.