Repository navigation
Description
Goal
Admin can create, edit, publish, schedule, and delete blog posts through the existing admin panel. Blog post body is edited via Tiptap WYSIWYG editor with image insertion. Supports draft/publish/scheduled workflows with multilingual content (uk/en/pl).
Scope
Admin routes
app/[locale]/admin/blog/page.tsx— post list table (sort: drafts first, then by updatedAt DESC)app/[locale]/admin/blog/new/page.tsx— create postapp/[locale]/admin/blog/[id]/page.tsx— edit postapp/[locale]/admin/blog/[id]/preview/page.tsx— preview post (locale-aware, any status)
Components (components/admin/blog/)
-
BlogPostListTable.tsx— responsive table (desktop + mobile cards):- Status badges: Draft (amber) / Scheduled (sky) / Published (emerald)
- Actions: Publish/Unpublish toggle (fixed width), Preview (new tab), Edit, Delete (disabled with tooltip for published posts)
- Centered Actions column
-
BlogPostForm.tsx— combined create/edit form:- Locale tabs (uk/en/pl) for: title (text input) + body (Tiptap editor)
- Single-locale fields: slug (auto-generated from EN title, editable), tags (comma-separated), resourceLink
- Category checkboxes + inline creation (InlineBlogCategoryForm)
- Author dropdown + inline creation (InlineBlogAuthorForm)
- Main image upload (via BlogImageUpload)
- Publish controls (BlogPublishControls)
- Validation: all 3 locale titles + bodies required (no fallback to EN)
- Edit mode: dirty tracking via JSON snapshot comparison (useRef), button disabled until changes made
- Create mode: button disabled until required fields valid
-
BlogTiptapEditor.tsx— Tiptap instance with:- Extensions: StarterKit, CodeBlockLowlight (github-dark theme), @tiptap/extension-image, @tiptap/extension-link, TaskList + TaskItem (checklists)
- Toolbar (sticky): Bold | Italic | Strike | Code | H2 | H3 | Blockquote | HR | Bullet List | Ordered List | Checklist | Code Block | Link | Image
- Image button: opens file picker → uploads via POST /api/admin/blog/images → inserts Cloudinary URL
-
BlogImageUpload.tsx— file input for main image, uploads to Cloudinary, previews result. Supports initialUrl for edit mode. -
BlogPublishControls.tsx— publish workflow:- "Save Draft" —
isPublished: false - "Publish Now" —
isPublished: true,scheduledPublishAt: null - "Schedule" —
isPublished: true,scheduledPublishAt: <datetime picker> - Shows current status when editing existing post
- "Save Draft" —
-
InlineBlogCategoryForm.tsx— inline 3-locale category creation from post form -
InlineBlogAuthorForm.tsx— inline 3-locale author creation from post form
API routes (app/api/admin/blog/)
route.ts— POST (create with publish mode handling)[id]/route.ts— PUT (update), DELETE (draft only), PATCH (publish toggle +revalidatePath)images/route.ts— POST: receive File →uploadImage(file, { folder: 'blog/posts' })→ return{ url, publicId }categories/route.ts— POST: inline category creationauthors/route.ts— POST: inline author creation
All routes: origin guard + CSRF validation + admin auth. Protected via layout guardAdminPage().
Upsert pattern for translations (Neon HTTP, no transactions):
db.insert(blogPostTranslations).values({ postId, locale, title, body }).onConflictDoUpdate({ target: [postId, locale], set: { title, body } })
Query layer — db/queries/blog/admin-blog.ts
getAdminBlogList()— all posts sorted by isPublished ASC, updatedAt DESCgetAdminBlogPostById(id)— single post with all locale translations + categoryIdscreateBlogPost()— insert post + translations + category junctionsupdateBlogPost()— update post + upsert translations + sync categoriesdeleteBlogPost()— delete (cascade handles translations + categories)toggleBlogPostPublish()— set isPublished + publishedAt + scheduledPublishAtgetAdminBlogAuthors()/getAdminBlogCategories()— dropdown datacreateBlogCategory()/createBlogAuthor()— inline creation
Validation — lib/validation/admin-blog.ts
createBlogPostSchema— Zod schema with 3-locale translations, nullable fields, publishMode enum, schedule date refinecreateBlogCategorySchema/createBlogAuthorSchema— inline creation schemas
BlogPostRenderer update — components/blog/BlogPostRenderer.tsx
- Added
taskListnode rendering (no bullets, space-y-3,[&_li_p]:mb-0) - Added
taskItemnode rendering (flex + baseline alignment, checkmark icon)
AdminSidebar update
Added Blog section to NAV_SECTIONS in components/admin/AdminSidebar.tsx:
- Posts → /admin/blog
- New Post → /admin/blog/new
- Authors → /admin/blog/authors
- Categories → /admin/blog/categories
Delete constraint: button always visible, disabled with tooltip ("Unpublish first to delete") for published posts. Draft posts can be deleted with confirmation.
Verification (all passed):
- Create post → save as draft → NOT visible on public /blog
- Publish immediately → appears on /blog after revalidate
- Edit published post → changes reflected after revalidate
- Unpublish from list page → status changes to Draft
- Delete draft → removed
- Preview draft → renders correctly with locale switching
- Inline category/author creation → appears in dropdowns immediately
- Dirty tracking → Update button disabled until changes made
- Task list items → render as checklists (not bullets) on preview
- Code blocks → syntax highlighting with github-dark theme
Expected impact
Admin can write and manage all blog content without Sanity. Full draft → schedule → publish workflow. Preview before publishing. Inline category/author creation without leaving the form.
Out of scope
- Author management (Issue (SP: 2) [Frontend] Admin Blog: Authors and Categories management #388)
- Category management (Issue (SP: 2) [Frontend] Admin Blog: Authors and Categories management #388)
- Pagination on list page (not needed with current post count)
Activity
feat(blog-admin): full blog post admin with Tiptap editor and publish…
(SP: 5) [Frontend] Full blog post Admin with Tiptap editor and publis…