Last Updated: 2026-04-02 Status: Active
This document outlines the detailed user interface structure, page layouts, and component interactions for the Blog Engine within apps/seller-dashboard (Next.js). It covers the admin authoring experience and the SDK-powered public consumption layer.
1. Primary Navigation
The Blog Engine lives under the main Dashboard sidebar as a top-level navigation item labeled "Blog".
Dashboard
├── ...
├── Blog
│ ├── All Posts
│ ├── Categories
│ ├── Tags
│ └── Integration Guide
├── Content Engine
├── AI Assistant
└── SettingsNOTE
The "Integration Guide" links to the embedded SDK documentation builder at /dashboard/docs/blog.
2. Page Specifications
2.1 Blog Post List (All Posts)
Path: /dashboard/blog
Purpose: Central hub for managing all blog posts across statuses. The workspace owner sees every post at a glance and can create, edit, or manage publication state.
UI Components:
-
Header Bar:
- Page title: "Blog Posts" with subtitle "Manage and publish your content."
- "Categories" button (outline) — navigates to
/dashboard/blog/categories - "Tags" button (outline) — navigates to
/dashboard/blog/tags - "Integration Guide" button — navigates to
/docs/blog(opens in new tab) - "Create New Post" button — permission-gated (
blog:posts.create), navigates to/dashboard/blog/new
-
Filter Bar (below header, above grid):
- Category Filter —
Selectdropdown listing all tenant categories with post counts. "All Categories" clears the filter. Filters via server-side?category=slugquery param. - Tag Filter —
Selectdropdown listing all tenant tags with post counts. "All Tags" clears the filter. Filters via server-side?tag=slugquery param. - Status Filter —
Selectdropdown:All Status|Draft|Published. Filters via?status=query param.
- Category Filter —
-
Post Grid (
md:grid-cols-2,xl:grid-cols-3):- Each card displays:
- Status Badge —
Published(green) orDraft(amber) - Category Badge — Displays assigned category name (if any), styled as a muted pill
- Tag Pills — Up to 3 tags displayed as small pills below the title,
+N moreif truncated - Post Title — truncated to 2 lines
- SEO Description — truncated to 3 lines, falls back to excerpt
- Last Updated — relative timestamp
- Status Badge —
- Cards link to
/dashboard/blog/{id}for editing - Hover effect: subtle shadow elevation and border highlight
- Each card displays:
-
Empty State:
- Illustration with message: "No blog posts yet"
- Prominent "Create your first post" CTA
-
Loading State:
- Skeleton grid matching the card layout
-
Error State:
- Error message with retry action
State Management: Posts are fetched via fetchBlogPosts(params?) on mount and whenever filters change. Categories and tags are fetched on mount for the filter dropdowns. All stored in the Redux blog slice (posts[], categories[], tags[], loading, error).
2.2 Blog Post Editor
Path: /dashboard/blog/new (create) | /dashboard/blog/{id} (edit)
Purpose: Full-featured content authoring environment for creating and editing blog posts. Supports rich text editing, media embedding, SEO optimization, and publication lifecycle management.
Editor Layout
Header Bar
| Element | Behavior |
|---|---|
| Back Button | Returns to post list. Shows confirmation dialog if unsaved changes exist. |
| Title Input | Large, borderless text field. Placeholder: "Post title..." |
| Status Badge | Displays Draft (amber) or Published (green) based on current state. |
| Autosave Indicator | Shows Saving..., All changes saved, or Unsaved changes with visual feedback. |
| Delete | Permission-gated (blog:posts.delete). Opens confirmation dialog. |
| Publish / Unpublish | Permission-gated (blog:posts.publish). Toggles publication state with confirmation. |
| Save | Permission-gated (blog:posts.update). Manually triggers save. |
Editor Tab — Rich Text Editor (TipTap)
The editor is powered by TipTap with a custom extension stack:
-
BlogToolbar — Fixed toolbar above the editor canvas:
- History: Undo / Redo
- Style dropdown: Normal text, Heading 1–6
- Inline formatting: Bold, Italic,
Strikethrough,Code - Lists: Bullet, Ordered, Task list
- Blocks: Blockquote, Code block, Table
- Media: Link, Image (via MediaDrawer), YouTube embed
-
Bubble Menu — Appears on text selection for quick inline formatting (bold, italic, strikethrough, code, link)
-
Floating Menu — Appears on empty lines for block-level insertion
-
Slash Commands — Type
/to open a searchable command palette:- Headings (H1–H3)
- Lists (bullet, ordered, task)
- Blocks (blockquote, code block, horizontal rule)
- Media (image, YouTube)
- Table
-
Block Drag Handle — Appears on hover for drag-and-drop block reordering
-
Table Controls — Row/column add/delete controls when a table is focused
-
Footer Bar — Displays word count and estimated reading time
-
Autosave — 30-second debounce in edit mode. Saves
contentJson(TipTap JSON) to the backend.
Settings & SEO Tab
| Component | Description |
|---|---|
| Featured Image Picker | Shows current image preview (recommended: 1200×630px). "Change" opens MediaDrawer, "Remove" clears the image. Empty state shows dashed border placeholder. |
| Category Selector | Dropdown listing all tenant categories. Single-select. Includes an inline "Create Category" option at the bottom that opens a name input without leaving the editor. Selecting a category assigns categoryId on the post. |
| Tag Input | Combobox with autocomplete from existing tenant tags. Type to search, press Enter or click to add. Tags appear as removable pills below the input. New tag names are created on-the-fly via upsertTags(). Maximum 10 tags per post. |
| URL Slug | Editable slug field (edit mode only). Shows warning: changing the slug breaks existing links. Validates format on save. |
| Post Excerpt | Textarea for a short summary. Used in blog list cards and meta descriptions. |
| SEO Title | Input with recommendation: 50–60 characters. |
| SEO Description | Textarea with recommendation: 150–160 characters. |
2.3 Category Management
Path: /dashboard/blog/categories
Purpose: Manage the taxonomy of blog posts. Categories are single-select, tenant-scoped, and provide the primary organizational axis for blog content.
Category List View
UI Components:
-
Header Bar:
- Page title: "Categories"
- Back button — navigates to
/dashboard/blog - "Create Category" button — permission-gated (
blog:posts.create), opens the create dialog
-
Category Table:
- Columns: Name, Slug, Posts (count of posts using this category), Created, Actions
- Actions column: Edit (pencil icon) and Delete (trash icon)
- Rows are sorted alphabetically by name
-
Create / Edit Dialog (shadcn
Dialog):- Name Input — required, supports Enter to submit
- Save — creates or updates the category. Slug is auto-generated server-side via
slugify(name). - Validation: name must be unique within the tenant (backend returns 409 on slug conflict)
-
Delete Confirmation:
- Dialog: "Deleting this category will remove it from N posts. The posts themselves will not be deleted. Continue?"
- Backend sets
categoryId = nullon affected posts (onDelete: 'set null')
-
Empty State:
- Message: "No categories yet. Create your first category to organize your blog posts."
2.4 Tag Management
Path: /dashboard/blog/tags
Purpose: Manage the flat tag taxonomy. Tags are multi-select, tenant-scoped, and provide a secondary cross-cutting organizational axis.
UI Components:
-
Header Bar:
- Page title: "Tags"
- Back button — navigates to
/dashboard/blog - "Create Tag" button — permission-gated (
blog:posts.create), opens the create dialog
-
Tag Table:
- Columns: Name, Slug, Posts (count of posts using this tag), Actions
- Actions column: Edit (pencil icon) and Delete (trash icon)
- Rows are sorted alphabetically by name
-
Create / Edit Dialog (shadcn
Dialog):- Name Input — required, supports Enter to submit
- Validation: name must be unique within the tenant (backend returns 409 on slug conflict)
-
Delete Confirmation (shadcn
AlertDialog):- Dialog: "Deleting "tag-name" will remove it from N post(s). The posts themselves will not be deleted."
- Backend cascades delete through
blog_post_tagsjunction table (onDelete: 'cascade')
-
Empty State:
- Message: "No tags yet. Tags are created automatically when you add them to posts, or create them here."
NOTE
Tags can also be created inline from the post editor's tag input. The tag management page is for dedicated cleanup, renaming, and auditing usage across posts.
2.5 Integration Guide (SDK Documentation)
Path: /dashboard/docs/blog
Purpose: Embedded documentation that teaches workspace owners how to integrate the blog into their customer-facing website using the @vlozi/blog SDK.
Layout: Two-column — DocSidebar (left) + DocLayout (right) with lazy-loaded chapters.
Chapters:
| Section | Chapters |
|---|---|
| Getting Started | Introduction, Quick Start, Core Concepts |
| Building | React Components, Hooks & Logic |
| Reference | API Reference, Raw HTTP API, Examples & Recipes |
Each chapter is lazy-loaded with Suspense boundaries and loading skeletons.
3. Component Architecture
3.1 Module File Structure
src/modules/blog/
├── api/
│ ├── blog.admin.api.ts # All admin API functions (posts + categories + tags)
│ └── proxy.ts # Next.js API route proxy
├── components/
│ ├── CategorySelector.tsx # Category dropdown for editor
│ ├── TagInput.tsx # Tag autocomplete combobox for editor
│ ├── FeaturedImagePicker.tsx # Featured image selector
│ └── SeoPanel.tsx # SEO metadata inputs
├── editor/
│ ├── BlogEditorCore.tsx # TipTap editor wrapper
│ ├── BlogToolbar.tsx # Formatting toolbar
│ ├── EditorBubbleMenu.tsx # Inline formatting popup
│ ├── EditorFloatingMenu.tsx # Empty-line action menu
│ ├── SlashCommand.tsx # / command palette
│ ├── CommandList.tsx # Command list UI
│ ├── BlockDragHandle.tsx # Block drag-and-drop
│ ├── TableControls.tsx # Table manipulation
│ ├── LinkDialog.tsx # URL insertion modal
│ ├── YouTubeDialog.tsx # YouTube embed modal
│ ├── tiptap.extensions.tsx # Extension configuration
│ └── nodes/
│ └── ImageNodeView.tsx # Custom image node
├── hooks/
│ └── useBlogStore.ts # Redux state hook (posts, categories, tags)
├── pages/
│ ├── BlogList.tsx # Post listing page (with filters)
│ ├── BlogEditor.tsx # Post editor page
│ ├── CategoryList.tsx # Category management page
│ └── TagList.tsx # Tag management page
└── store/
├── blog.slice.ts # Redux slice
└── blog.types.ts # State types (BlogPostSummary, BlogCategory, BlogTag)Next.js Route Files:
src/app/dashboard/blog/
├── page.tsx # /dashboard/blog
├── BlogListWrapper.tsx
├── new/
│ ├── page.tsx # /dashboard/blog/new
│ └── BlogEditorWrapper.tsx
├── [id]/
│ ├── page.tsx # /dashboard/blog/:id
│ └── BlogEditorWrapper.tsx
├── categories/
│ ├── page.tsx # /dashboard/blog/categories
│ └── CategoryListWrapper.tsx
└── tags/
├── page.tsx # /dashboard/blog/tags
└── TagListWrapper.tsx3.2 State Management
Redux Slice Shape:
interface BlogState {
posts: BlogPostSummary[];
categories: BlogCategory[];
tags: BlogTag[];
loading: boolean;
error?: string;
}Actions: setLoading(), setPosts(), setCategories(), setTags(), setError(), resetBlog()
3.3 API Layer
All admin API calls go through the Next.js proxy (/api/blog/*) which injects the JWT from NextAuth before forwarding to the gateway.
Post Endpoints:
| Function | Method | Endpoint |
|---|---|---|
fetchBlogPosts() |
GET | /api/blog/admin/posts |
fetchPostById(id) |
GET | /api/blog/admin/posts/{id} |
createPost(data) |
POST | /api/blog/admin/posts |
updatePost(id, data) |
PUT | /api/blog/admin/posts/{id} |
publishPost(id) |
POST | /api/blog/admin/posts/{id}/publish |
unpublishPost(id) |
POST | /api/blog/admin/posts/{id}/unpublish |
deletePost(id) |
DELETE | /api/blog/admin/posts/{id} |
NOTE
createPost and updatePost payloads include categoryId (string or null, optional) and tags (string[], optional). The backend calls upsertTags() to resolve tag names to IDs, inserts junction rows in blog_post_tags, and sets categoryId directly as an FK on the post.
Category Endpoints:
| Function | Method | Endpoint |
|---|---|---|
fetchCategories() |
GET | /api/blog/admin/categories |
createCategory(data) |
POST | /api/blog/admin/categories |
updateCategory(id, data) |
PUT | /api/blog/admin/categories/{id} |
deleteCategory(id) |
DELETE | /api/blog/admin/categories/{id} |
Tag Endpoints:
| Function | Method | Endpoint |
|---|---|---|
fetchTags() |
GET | /api/blog/admin/tags |
createTag(data) |
POST | /api/blog/admin/tags |
updateTag(id, data) |
PUT | /api/blog/admin/tags/{id} |
deleteTag(id) |
DELETE | /api/blog/admin/tags/{id} |
4. SDK Components (Public Consumption)
The @vlozi/blog package provides pre-built React components and hooks for customer websites.
Canonical reference:
sdk-reference.mdis the ground-truth for all SDK APIs. This section is a quick overview; consult that doc for complete prop tables and edge cases.
4.1 Setup & Provider
import { VloziClient } from '@vlozi/blog';
import { VloziProvider } from '@vlozi/blog/react';
const client = new VloziClient({
apiKey: 'your-api-key',
baseUrl: 'https://your-gateway-url.com',
});
<VloziProvider client={client}>
<App />
</VloziProvider>4.2 Available Hooks
| Hook | Input | Output |
|---|---|---|
usePosts({ page?, limit?, category?, tag?, search?, sort?, order? }) |
ListParams | { data: PaginatedResponse<Post>, loading, error, refetch, page, totalPages, hasNextPage, hasPrevPage } |
usePost(slug) |
Post slug | { data: Post, loading, error } |
useCategories() |
— | { data: Category[], loading, error, refetch } |
useTags() |
— | { data: Tag[], loading, error, refetch } |
4.3 Available Components
| Component | Props | Description |
|---|---|---|
<BlogList> |
limit, columns (1–4), variant (grid/list), category, tag, showPagination, renderItem, renderLoading, renderEmpty, renderError |
Paginated post grid with optional category/tag filtering |
<BlogCard> |
post, variant (default/featured/compact), renderMeta, footer, onClick |
Individual post card (includes category badge and tag pills) |
<BlogPost> |
slug, renderTitle, showFeaturedImage, className |
Full article renderer with HTML sanitization |
<BlogCategoryNav> |
activeCategory, onSelect, variant (sidebar/tabs/pills), showCounts |
Category navigation — fetches categories automatically |
<BlogTagNav> |
activeTag, onSelect, variant (pills/cloud), showCounts |
Tag navigation — fetches tags automatically |
4.4 SDK Types
interface Post {
title: string;
slug: string;
excerpt: string;
content?: string;
publishedAt: string;
seoTitle?: string;
seoDescription?: string;
featuredImageUrl?: string | null;
category?: Category | null;
tags?: Tag[];
readingTime?: number;
}
interface Category {
name: string;
slug: string;
postCount?: number;
}
interface Tag {
name: string;
slug: string;
postCount?: number;
}
interface ListParams {
page?: number;
limit?: number;
category?: string;
tag?: string;
search?: string;
sort?: 'publishedAt' | 'title' | 'createdAt';
order?: 'asc' | 'desc';
}4.5 SDK Client Methods
client.blog.list({ page?, limit?, category?, tag?, search?, sort?, order? })
client.blog.get(slug)
client.blog.categories.list()
client.blog.tags.list()IMPORTANT
The SDK's BlogPost component uses a regex-based HTML sanitizer. For production deployments, integrators should install dompurify for robust XSS protection.
5. Permission Model
All blog actions in the seller dashboard are gated by granular permissions:
| Permission | Grants Access To |
|---|---|
blog:posts.read |
View post list, individual posts, categories list, tags list, filter dropdowns |
blog:posts.create |
"Create New Post" button, "Create Category" button, "Create Tag" button, inline category creation in editor |
blog:posts.update |
Save button, autosave, slug editing, category/tag assignment, edit category/tag name |
blog:posts.delete |
Delete post, delete category, delete tag |
blog:posts.publish |
Publish / Unpublish toggle |
system:owner |
Bypasses all permission checks |
NOTE
Categories and tags reuse blog:posts.* permission scopes. This avoids needing separate RBAC roles — any user who can manage posts can also manage categories and tags.
6. Responsive Design
The blog editor is primarily a desktop experience, but the post list adapts to smaller screens.
| Breakpoint | Behavior |
|---|---|
| Desktop (>1280px) | 3-column post grid. Full-width editor with side-by-side toolbar. |
| Tablet (768–1280px) | 2-column post grid. Editor toolbar wraps to multiple rows. |
| Mobile (<768px) | Single-column post grid. Editor toolbar becomes a scrollable strip. Settings tab stacks vertically. |
7. Confirmation Dialogs & Safety
The editor employs multiple safety nets to prevent accidental data loss:
| Trigger | Dialog |
|---|---|
| Back with unsaved changes | "You have unsaved changes. Are you sure you want to leave?" |
| Delete post | "This action cannot be undone. Are you sure you want to delete this post?" |
| Publish post | "Publishing will make this post visible to your customers. Continue?" |
| Unpublish post | "Unpublishing will hide this post from your website. Continue?" |
| Change slug | Warning text: "Changing the URL slug will break any existing links to this post." |
| Browser close/refresh | beforeunload event fires if there are unsaved changes. |