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.