From a Buggy contentEditable to a Real Editor: Debugging My Blog Module
Pasting AI-generated content wiped the editor. I'd copy a draft from an AI chat, paste it into the write-post area, and instead of adding to what I'd already written, it would just clear everything out. Not append. Not insert. Just gone.
The Encounter
I've been building a blog module for a Next.js project — the usual stuff: title, slug, cover image, excerpt, and a content editor for the actual post body. Simple on paper. I'd built the editor myself using a custom contentEditable div, and it mostly worked... until it didn't.
Three things broke on me today, all in the same corner of the app:
- Pasting AI-generated content wiped the editor. I'd copy a draft from an AI chat, paste it into the write-post area, and instead of adding to what I'd already written, it would just clear everything out. Not append. Not insert. Just gone.
- Formatting options silently failed. Each line had a small menu on the right for things like headings and lists. I'd click an option and... nothing. No visible change. I couldn't even tell if it was actually applying and just not re-rendering, or if it wasn't applying at all.
- No undo. Once a bad paste happened, Ctrl+Z did nothing useful. I was stuck manually cleaning up.
Regular plain-text paste worked fine, which was the clue that mattered most — the bug wasn't in "pasting" in general, it was specifically in how my custom editor was handling structured content and re-syncing its own state afterward.
Why It Was Happening
Custom contentEditable implementations look deceptively simple to build and are notoriously hard to get right. The browser's own DOM mutation on paste and the React state tracking my content were fighting each other — React would re-render based on what it thought the state was, stomping over what the browser had just inserted. And because I hadn't built any real undo history, there was nothing to fall back to when that happened.
Formatting options not applying live pointed to the same root issue: my UI was updating state, but the actual DOM (and by extension, what got saved) wasn't reliably reflecting it.
How I Fixed It
Instead of continuing to patch a fragile custom editor, I made the call to replace it with Editor.js — an open-source, block-based editor (the kind of experience Notion or Google Docs give you) that handles exactly the problems I was running into:
- Structured paste handling. Editor.js parses incoming HTML/markdown into proper blocks instead of dumping raw text into one div and hoping the DOM stays in sync.
- A real toolbar. Inline formatting (bold, italic, links) and block-level options (headings, lists, quotes) are first-class, tested features — not something I'm hand-rolling with a menu that may or may not reflect the actual document state.
- Built-in undo, via the
@editorjs/undoplugin, so a bad paste is one keystroke away from being reversed.
I also added a safety net on top of that: a Markdown mode. Instead of relying entirely on live paste-interception (which is where the fragility lives), there's now a toggle to switch into a plain textarea, paste or type raw content freely with zero risk of wiping anything, and hit "Convert to blocks" when it's ready. If the conversion looks off, I just edit the text and reconvert — no fighting an undo stack, no guessing what the editor did with my paste.
While I was in there, I also tightened up the rest of the stack:
- Split image handling out to Cloudinary, with a dedicated delete function so removed or replaced images don't just sit around orphaned in storage
- Moved from one flat MongoDB collection to a normalized schema — separate
Post,Image,Author,Category,Tag, andSeoMetacollections, properly referenced — while keeping the actual post content embedded as an ordered block array, since that's always read as a single sequence and doesn't benefit from being split apart - Added a caching layer, a single consistent API response format, and centralized error handling, so the module isn't just "working" but is actually structured to be dropped into another project without a rewrite
The Takeaway
The bug wasn't really about paste events or toolbars — it was about trusting a hand-built editor to manage document state as reliably as a purpose-built one. Sometimes the fix for "it's a little buggy but it works" isn't another patch. It's recognizing you're re-solving a problem a mature open-source tool already solved well, and letting it do that job instead.