Tutorial

How to Build a React Blog Editor with Markdown Support (v2.0.10)

What We Are Building

A React blog post editor with four panels: a rich text WYSIWYG editor, a Markdown view toggle, a live HTML preview, and a publish action. Content is stored as both HTML (for display on the frontend) and Markdown (for storage in a headless CMS or Git repository).

Step 1: Install Dependencies

npm install @rohanyeole/ray-editor-react @rohanyeole/ray-editor

Step 2: Create the Editor Component

// components/BlogEditor.jsx
import React, { useRef, useState } from 'react';
import { RayEditor } from '@rohanyeole/ray-editor-react';
import '@rohanyeole/ray-editor/dist/ray-editor.css';

const TOOLBAR = [
  'bold','italic','underline','strikethrough',
  'headings','blockquote','orderedList','unorderedList','taskList',
  'link','imageUpload','table','codeBlock',
  '|',
  'markdownToggle','exportMarkdown','importMarkdown',
  '|',
  'findReplace','wordCount','fullscreen'
];

export default function BlogEditor({ initialContent = '', onSave }) {
  const [html, setHtml] = useState(initialContent);
  const editorRef = useRef(null);

  const handleSave = () => {
    const markdown = editorRef.current?.exportMarkdown() ?? '';
    onSave({ html, markdown });
  };

  return (
    <div className="blog-editor">
      <RayEditor
        ref={editorRef}
        value={html}
        onChange={setHtml}
        options={{
          toolbar: TOOLBAR,
          placeholder: 'Write your post...',
          darkMode: 'auto',
          maxHeight: '600px'
        }}
      />
      <div className="editor-actions">
        <button onClick={handleSave}>Save Post</button>
      </div>
    </div>
  );
}

Step 3: Live Preview Panel

// components/BlogPreview.jsx
export default function BlogPreview({ html }) {
  return (
    <div className="blog-preview">
      <h2>Preview</h2>
      <div
        className="prose"
        dangerouslySetInnerHTML={{ __html: html }}
      />
    </div>
  );
}

Step 4: Assemble the Full Editor Page

// pages/new-post.jsx (Next.js) or App.jsx
import dynamic from 'next/dynamic';
import BlogPreview from '../components/BlogPreview';
import { useState } from 'react';

// Lazy-load to avoid SSR issues
const BlogEditor = dynamic(() => import('../components/BlogEditor'), { ssr: false });

export default function NewPostPage() {
  const [content, setContent] = useState({ html: '', markdown: '' });

  const handleSave = async ({ html, markdown }) => {
    setContent({ html, markdown });
    // POST to your CMS or API
    await fetch('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ html, markdown })
    });
  };

  return (
    <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'24px' }}>
      <BlogEditor onSave={handleSave} />
      <BlogPreview html={content.html} />
    </div>
  );
}

Step 5: Word Count and Auto-Save

// Add word count display using the RayEditor API
import { useEffect, useRef } from 'react';

function WordCountBar({ editorRef }) {
  const [count, setCount] = useState({ words: 0, chars: 0 });

  useEffect(() => {
    const editor = editorRef.current?._editor; // internal instance
    if (!editor) return;
    const off = editor.on('content:change', () => {
      setCount(editor.getWordCount());
    });
    return () => off();
  }, [editorRef]);

  return <p>{count.words} words · {count.chars} chars</p>;
}

Headless CMS Integration

Store both HTML and Markdown from the onSave callback. Most headless CMSs accept HTML or Markdown strings. Use the Markdown version for Git-based CMS like Contentlayer or the HTML version for API-based CMSs like Contentful or Sanity.

Conclusion

With RayEditor and its official React wrapper, building a full-featured blog editor — WYSIWYG mode, Markdown mode, live preview, word count, and headless CMS integration — takes under 50 lines of React code. MIT licensed, zero dependencies, React 18 compatible.

Try RayEditor for free

No sign-up. No credit card. MIT licensed. Works in React, Vue, Angular, and Svelte.

See live demos