Use Case

    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.

    Fixed
    Toolbar for writing
    getText()
    Word count API
    Auto
    Save on change

    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

    Headings H1–H6 for structure
    Bold, italic, underline, strikethrough
    Ordered & unordered lists
    Blockquote formatting
    Links with inline URL editing
    Image insertion via insertHTML()
    Paste clean from Word & Google Docs
    getText() for live word count
    setHTML() for draft loading
    onChange autosave pattern
    Keyboard shortcut support
    Built-in XSS sanitization

    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.