editorcn pro

Blog Post Editor

A blog writing workspace with a cover image, SEO fields, Markdown import and export, a reader preview and draft autosave.

A blog writing workspace with a breadcrumb bar, a title header with draft status and last-saved time, and a docked settings panel. The editor card holds a cover image with alt text, a large title and subtitle, and a body editor with headings, lists, quotes, callouts, syntax-highlighted code blocks, images, tables and YouTube embeds. Markdown pasted from anywhere turns into formatted content, and pastes from Google Docs, Word or Notion come in clean. Switch to Preview to see the post as readers will, with the byline and reading time. The settings panel holds the URL slug, tag chips, SEO title and meta description, with character limits and a live search-result preview. Drafts autosave through onSave, and Publish hands the finished post to onPublish.

Installation

$ pnpm dlx shadcn@latest add @editorcn-pro/blog-post-editor

The CLI also installs the Markdown, Image Placeholder and Table extensions and the Static Renderer.

Usage

import { BlogPostEditor } from "@/components/templates/blog-post-editor";
 
export const PostEditor = ({ post, user }) => (
  <BlogPostEditor
    author={{ avatar: user.avatar, name: user.name }}
    initialContent={post.content}
    initialPost={post}
    onSave={(draft) => saveDraft(post.id, draft)}
    onPublish={(published) => publishPost(post.id, published)}
    onUploadImage={(file) => uploadToStorage(file)}
    siteUrl="acme.dev/blog"
  />
);

onSave and onPublish both get a BlogPost:

interface BlogPost {
  title: string;
  subtitle: string;
  slug: string;
  tags: string[];
  cover: { src: string; alt: string } | null;
  metaTitle: string;
  description: string;
  status: "draft" | "published";
  content: JSONContent;
  markdown: string;
}

content is Tiptap JSON, ready for the Static Renderer. markdown is the full post as a Markdown file with front matter, so you can commit it straight to an MDX or Markdown blog. The slug falls back to the title when left empty.

onSave runs 800ms after the last change. Return a promise to keep the status on "Saving…" until it resolves; a rejected promise shows "Couldn't save". onPublish works the same way, and the post switches to Published once it resolves.

Markdown

initialContent accepts a Markdown string or Tiptap JSON. ⋯ → Import Markdown reads a file's front matter (title, subtitle, slug, tags, cover, metaTitle, description) into the post fields. Without front matter, a leading # Heading becomes the title. Export Markdown and Copy as Markdown write the same format back:

---
title: "How we made checkout 3x faster"
slug: "how-we-made-checkout-3x-faster"
tags: ["performance", "frontend"]
---
 
Last spring our checkout page took **4.8 seconds**...

Covers stored as data URLs are left out of the front matter. Callouts round-trip as GitHub alerts (> [!NOTE], > [!TIP], > [!WARNING]).

Images

Without onUploadImage, covers are stored as data URLs. That works for demos, but large posts get heavy, so pass an upload function that returns a hosted URL for production.

Props

PropTypeDescription
initialContentstring | JSONContentMarkdown string or Tiptap JSON for the body
initialPostPartial<BlogPost>Title, subtitle, slug, tags, cover, SEO, status
author{ name, avatar? }Byline shown in the header and preview
breadcrumbsstring[]Path shown above the title, default Blog, Posts
onSavefunctionGets the BlogPost after edits settle
onPublishfunctionGets the BlogPost with status: "published"
onUploadImage(file) => Promise<string>Uploads a cover and returns its URL
siteUrlstringShown in the slug hint and search preview
classNamestringOuter container classes