Blog Post Editor for JavaScript Apps
Build a polished blog writing experience with Scribe: fixed toolbar, autosave with debounce, draft loading via setHTML(), live word count via getText(), and safe paste from Word and Google Docs — all under 50KB.
Everything a blog editor needs
Full formatting suite
Headings H1–H6, bold, italic, blockquote, ordered and unordered lists, links, inline code, and image embedding. Everything a blog post needs.
Autosave + draft support
Wire onChange to a debounced save call. Load existing drafts with setHTML(). The editor state survives page refreshes with your API as the source of truth.
getText() for word count
Call editor.getText() to get plain text for live word count, reading time estimate, or SEO excerpt generation without parsing HTML.
Paste from Word & Google Docs
Blog writers paste content from all over. Scribe strips proprietary markup automatically, keeping your HTML clean and consistent.
Blog Editor with Autosave
Load a draft, enable autosave with debounce, and publish via a single getHTML() call.
import { Scribe } from 'scribejs-editor';
import 'scribejs-editor/dist/scribe.css';
const editor = Scribe.init('#blog-content', {
toolbar: 'fixed',
autofocus: true,
placeholder: 'Start writing your post...',
onChange: (html) => autosave(html),
});
// Load draft from server
const draft = await fetch(`/api/drafts/${postId}`).then(r => r.json());
if (draft.content) editor.setHTML(draft.content);
// Autosave with debounce
let saveTimer;
function autosave(html) {
clearTimeout(saveTimer);
saveTimer = setTimeout(async () => {
await fetch(`/api/drafts/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: html }),
});
document.querySelector('#save-status').textContent = 'Saved';
}, 2000);
}
// Publish
document.querySelector('#publish-btn').addEventListener('click', async () => {
const html = editor.getHTML();
await fetch(`/api/posts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: html, title }),
});
window.location.href = '/admin/posts';
});React Blog Editor Component
A complete React component with autosave, status indicator, and publish action.
import { useRef, useEffect, useState, useCallback } from 'react';
import { Scribe } from 'scribejs-editor';
export function BlogPostEditor({ postId, initialContent, title }) {
const containerRef = useRef(null);
const editorRef = useRef(null);
const [status, setStatus] = useState('');
const saveTimer = useRef(null);
useEffect(() => {
editorRef.current = Scribe.init(containerRef.current, {
toolbar: 'fixed',
autofocus: true,
placeholder: 'Write your story...',
onChange: scheduleAutosave,
});
if (initialContent) editorRef.current.setHTML(initialContent);
return () => editorRef.current?.destroy();
}, [postId]);
const scheduleAutosave = useCallback((html) => {
clearTimeout(saveTimer.current);
setStatus('Saving...');
saveTimer.current = setTimeout(async () => {
await fetch(`/api/drafts/${postId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: html }),
});
setStatus('Draft saved');
}, 2000);
}, [postId]);
const publish = async () => {
const html = editorRef.current?.getHTML();
if (!html || editorRef.current?.isEmpty()) return;
await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: html, title }),
});
};
return (
<div className="blog-editor">
<div className="toolbar-wrapper">
<span className="save-status">{status}</span>
<button onClick={publish} className="btn-publish">Publish</button>
</div>
<div ref={containerRef} className="prose prose-lg max-w-none" />
</div>
);
}Live Word Count & Keyboard Shortcuts
Use editor.getText() for live word count and reading time. Add keyboard shortcuts for power writers.
import { Scribe } from 'scribejs-editor';
const editor = Scribe.init('#content', {
toolbar: 'fixed',
onChange: updateStats,
});
// Live word count and reading time
function updateStats(html) {
const text = editor.getText(); // plain text
const words = text.trim().split(/\s+/).filter(Boolean).length;
const chars = text.length;
const readTime = Math.ceil(words / 200); // ~200 wpm
document.querySelector('#word-count').textContent = `${words} words`;
document.querySelector('#char-count').textContent = `${chars} chars`;
document.querySelector('#read-time').textContent = `~${readTime} min read`;
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
const mod = e.metaKey || e.ctrlKey;
if (!mod) return;
if (e.key === 'b') { e.preventDefault(); editor.bold(); }
if (e.key === 'i') { e.preventDefault(); editor.italic(); }
if (e.key === 'k') { e.preventDefault(); editor.link(prompt('URL:') ?? ''); }
if (e.key === 's') { e.preventDefault(); save(); }
});Everything a blog writer expects
Ship your blog editor today.
Autosave, word count, full formatting, safe paste. Everything in 50KB — free under MIT.
Blog editor — common questions
How do I build a blog post editor in JavaScript?
Use Scribe Editor with toolbar: 'fixed' and an onChange callback that debounce-saves to your API. Load existing drafts with editor.setHTML(content). On publish, call editor.getHTML() to get the clean HTML and send it to your server. Scribe works with React, Vue, Svelte, Next.js, Nuxt, and plain Vanilla JS.
How do I add autosave to a blog editor?
In the Scribe onChange callback, clear a previous timeout and set a new one with a 2-3 second delay. When the timeout fires, send the HTML from editor.getHTML() to your draft API. This debounced autosave pattern ensures content is saved without hammering your server on every keystroke.
Can I get a word count from Scribe Editor?
Yes. Use editor.getText() to get the plain text content without HTML tags. Split it on whitespace to count words, get the length for character count, or divide by 200 for an estimated reading time. Update these values in the onChange callback for live stats.
Does Scribe Editor handle pasting from Word or Google Docs?
Yes. Scribe Editor has built-in paste sanitization that strips proprietary Word and Google Docs markup (conditional comments, class names, inline styles) and converts the content to clean semantic HTML. This means your blog writers can paste from any source and get consistent, clean output.
Also compare Scribe with:
Use Scribe in your framework:
Popular use cases: