logicspike/docs

Blog Engine

Blog Engine — Frontend & UI Specification

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
└── Settings

NOTE

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 FilterSelect dropdown listing all tenant categories with post counts. "All Categories" clears the filter. Filters via server-side ?category=slug query param.
    • Tag FilterSelect dropdown listing all tenant tags with post counts. "All Tags" clears the filter. Filters via server-side ?tag=slug query param.
    • Status FilterSelect dropdown: All Status | Draft | Published. Filters via ?status= query param.
  • Post Grid (md:grid-cols-2, xl:grid-cols-3):

    • Each card displays:
      • Status BadgePublished (green) or Draft (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 more if truncated
      • Post Title — truncated to 2 lines
      • SEO Description — truncated to 3 lines, falls back to excerpt
      • Last Updated — relative timestamp
    • Cards link to /dashboard/blog/{id} for editing
    • Hover effect: subtle shadow elevation and border highlight
  • 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 = null on 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_tags junction 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.tsx

3.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.md is 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.
Blog Engine