Blog Editor UX
The Blog Editor is the authoring surface used by Malet Owners to create, edit, and publish blog content on their Malets. The upgraded editor adds four key capabilities: auto-save drafts, Tiptap JSON validation, version history with restore, and a publish approval pipeline (DRAFT โ REVIEW โ APPROVED โ PUBLISHED/SCHEDULED).
NOTE
The blog editor is accessed from The Deck or directly via the Admin Dashboard. Blog content is managed by the blogs subgraph.
Architecture
The editor is separated into a store layer and a UI layer, following the Svelte 5 runes pattern used across all Mallnline dashboard components.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ BlogForm.svelte โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ Autosave โ โ History Drawer โ โ
โ โ Bar โ โ (slide-in) โ โ
โ โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ TiptapEditor.svelte โ
โ โ (ProseMirror + word count) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ Publish Pipeline Actions โ
โ โ Draft โ Review โ Publish โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ blogEditorStore.svelte.ts
โ (localStorage snapshots,
โ Zod JSON validation,
โ pipeline state machine)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Store: `blogEditorStore.svelte.ts`
A Svelte 5 runes class (BlogEditorStore) that manages:
- Content state โ title, slug, excerpt, coverImage, tags, html, json, stage
- Auto-save timer โ 30-second debounced timer triggered by
markDirty() - Draft snapshots โ localStorage-backed array of
DraftSnapshotobjects - Tiptap JSON validation โ Zod schema (
TiptapDocSchema) validates ProseMirror output - Publish pipeline โ state machine with
canTransition()guard +transitionTo()mutator
Component: `BlogForm.svelte`
The UI layer consumes the store and renders:
- Autosave indicator bar โ stage badge (color-coded), JSON validation badge (โ / โ), dirty/saved status, manual save button, history toggle
- Title input โ Headline-style 2.5rem input
- Meta panel โ URL slug (auto-generated from title), cover image uploader, excerpt textarea, tag input
- Editor โ TiptapEditor with real-time word/character count footer
- Schedule picker โ datetime-local input for scheduling publications
- Pipeline action bar โ context-sensitive buttons based on current stage
Auto-Save System
| Parameter | Value |
|---|---|
| Debounce delay | 30 seconds |
| Storage backend | localStorage (blog_drafts_{postId}) |
| Max snapshots | 20 per post (oldest trimmed) |
| Snapshot contents | title, html, json, excerpt, tags, coverImage, timestamp, stage |
| Manual save | ๐พ button in the autosave bar |
Each snapshot is a DraftSnapshot with a unique ID (snap_{timestamp}_{random}). When localStorage reaches capacity, the store gracefully degrades to keeping only the 5 most recent snapshots.
Tiptap JSON Validation
The TiptapEditor emits both HTML and ProseMirror JSON on every content change. The store validates the JSON against a Zod schema:
const TiptapDocSchema = z.object({
type: z.literal('doc'),
content: z.array(ProseMirrorNodeSchema).optional()
});
The validation badge in the autosave bar shows:
- โ Valid (green badge) โ document conforms to the ProseMirror spec
- โ Invalid (red badge) โ with a tooltip showing the Zod error message
TIP
The JSON validation uses z.object({}).passthrough() for ProseMirror node attrs instead of z.record() for Zod v4 compatibility. Nested content is validated one level deep; deeply nested nodes use passthrough to avoid circular schema issues.
Version History
The Draft History Drawer is a slide-in panel from the right edge of the viewport.
Drawer Features
- Reverse-chronological list of all saved snapshots
- Each card shows: title (truncated), timestamp, color-coded stage badge
- Restore โ replaces all editor state from the snapshot
- Delete โ removes a single snapshot
- Clear All โ wipes all snapshots for the post
Restore Behavior
When a Malet Owner restores a version:
- Store state is updated (title, html, json, excerpt, tags, coverImage)
- The TiptapEditor's
setJSON()method is called with the parsed ProseMirror JSON - If JSON parsing fails, falls back to
setContent()with the HTML string - The form becomes dirty (unsaved indicator appears)
Publish Approval Pipeline
A visual state machine governing the lifecycle of a blog post.
Stage Definitions
| Stage | Label | Color | Description |
|---|---|---|---|
DRAFT |
Draft | Gray (#6b7280) | Work in progress, not visible to Visitors |
REVIEW |
In Review | Amber (#f59e0b) | Submitted for team review |
APPROVED |
Approved | Green (#10b981) | Reviewed and approved for publication |
PUBLISHED |
Published | Indigo (#667eea) | Live on the Malet storefront |
SCHEDULED |
Scheduled | Purple (#8b5cf6) | Queued for publication at a future date/time |
Valid Transitions
DRAFT โโโโโ REVIEW โโโโโ APPROVED โโโโโ PUBLISHED
โ โ
โโโโโโ PUBLISHED โโโโโโ SCHEDULED
โโโโโโ SCHEDULED โโโโโโ DRAFT (send back)
PUBLISHED โโโ DRAFT (unpublish)
SCHEDULED โโโ DRAFT | PUBLISHED
Anyone with write permissions on the Malet can transition between stages. The pipeline is enforced visually โ context-sensitive action buttons appear based on the current stage:
- DRAFT: "Submit for Review", "Publish", "Schedule" buttons
- REVIEW: "Approve & Publish" button
- PUBLISHED: No additional actions (post is live)
IMPORTANT
The publish pipeline is currently a frontend-only workflow. The backend blogs subgraph stores the status field and accepts REVIEW and SCHEDULED values, but does not enforce role-based approval gates. Any user with write permissions can publish directly.
TiptapEditor Enhancements
The shared TiptapEditor.svelte component was enhanced for the blog editor:
| Feature | Prop/API | Description |
|---|---|---|
| JSON callback | onupdate({ html, json }) |
Emits both HTML and ProseMirror JSON on every change |
| Word count | showWordCount prop |
Footer bar with live word and character counts |
| JSON restore | setJSON(jsonObj) method |
Restores editor state from a ProseMirror JSON object |
GraphQL Changes
scheduledPublishAt was added to the Blog entity type, CreateBlogInput, and UpdateBlogInput in src/lib/queries/blog.ts. The REVIEW status was added to all status union types.
File Map
| File | Purpose |
|---|---|
src/stores/blogEditorStore.svelte.ts |
Store: auto-save, validation, pipeline state |
src/lib/components/blog/BlogForm.svelte |
UI: full blog editor with pipeline |
src/lib/components/editor/TiptapEditor.svelte |
Enhanced rich text editor |
src/lib/queries/blog.ts |
GraphQL mutations with scheduledPublishAt |
tests/blogEditorStore.test.ts |
34 unit tests |
Related
- Admin Analytics Dashboards โ Blog analytics charts and engagement KPIs consumed alongside the editor
- Edit History Audit Trail โ Field-level diff viewer tracking blog post changes
- Organization & Malet Management โ Malet ownership context and The Deck operations
- Alert Templates โ Companion template maker in the Admin Dashboard Templates tab