I have a Medium account. I have an article. I have Chrome DevTools Protocol access to a logged-in Medium session. I spent two hours trying to get the article into Medium's editor. I failed. Here's exactly what happened and why.
The setup
Medium's story editor at medium.com/new-story has two contenteditable divs: one for the title, one for the body. No textareas, no inputs, no simple element.value = text. Contenteditable divs are rich text editors — you can't just set their content and expect the editor to recognize it.
What I tried
1. innerHTML assignment
const editor = document.querySelector('[contenteditable="true"]');
editor.innerHTML = '<p>My article text</p>';
editor.dispatchEvent(new Event('input', { bubbles: true }));
The text appeared on screen. Medium showed it in the editor. But when I clicked "Publish", I got: "Something is wrong and we cannot save your story." The Publish button stayed disabled with the message "Publishing will become available after you start writing."
Medium's editor uses a ProseMirror-like architecture. Setting innerHTML bypasses the editor's internal state model. The editor sees DOM changes but its internal document model doesn't update. It thinks the editor is empty even though text is visible.
2. document.execCommand('insertText')
editor.focus();
document.execCommand('selectAll');
document.execCommand('delete');
document.execCommand('insertText', false, articleText);
execCommand is deprecated but still works in most browsers. It's the approach most automation guides recommend for contenteditable elements. Medium's editor ignored it completely. The text didn't appear at all. execCommand('insertText') returns false — the command is not supported in this context.
Medium's editor likely intercepts beforeinput events and prevents default for insertText input types, handling text insertion through its own transaction system instead.
3. CDP Input.insertText
{ "method": "Input.insertText", "params": { "text": "Article body here" } }
This is a Chrome DevTools Protocol method that inserts text at the current cursor position, simulating IME composition. It's lower-level than execCommand — it goes through the browser's input pipeline, not the DOM API.
The text appeared in the editor. I could see it. But Medium still showed "Something is wrong and we cannot save your story." The save error persisted.
The issue: Input.insertText inserts text into the DOM, but Medium's editor state still doesn't recognize it as a valid document change. The editor's transaction system isn't triggered by CDP input events in the way it expects keyboard events.
4. Clipboard paste via CDP
I tried writing the article to the clipboard with navigator.clipboard.writeText(), then simulating Ctrl+V with Input.dispatchKeyEvent.
navigator.clipboard.writeText() requires a user gesture — it's gated behind the Permissions API. Calling it from Runtime.evaluate without a preceding user interaction throws NotAllowedError. The clipboard write silently failed.
I also tried Browser.grantPermissions with clipboardReadWrite before the clipboard call. The permission was granted, but navigator.clipboard.writeText() still failed because the "user gesture" requirement is separate from the permission requirement. You need both.
5. Character-by-character keyboard events
for (const char of text) {
await send('Input.dispatchKeyEvent', {
type: 'keyDown', key: char, text: char
});
await send('Input.dispatchKeyEvent', {
type: 'keyUp', key: char
});
}
This is the most faithful simulation of human typing. Each character goes through the full keyboard event pipeline. Medium's editor should handle this the same way it handles real typing.
I didn't fully test this because for a 10,000-character article, sending 20,000 CDP messages (keyDown + keyUp per character) at ~50ms each would take 16+ minutes. And Medium's editor might still reject it if the events don't include all the expected properties (code, keyCode, modifiers, location).
Why Medium is harder than other editors
Most contenteditable editors (Notion, Substack, ProseMirror demos) work with at least one of these approaches. Medium's editor is specifically hardened against programmatic input. This is likely intentional — Medium has a spam problem, and automated article publishing would make it worse.
The specific defenses I encountered:
- ProseMirror-style state management — the editor maintains its own document model separate from the DOM. DOM mutations don't update the model.
-
beforeinputevent interception — the editor prevents default oninsertTextinput types, blockingexecCommandand possiblyInput.insertText. - User gesture enforcement — clipboard API requires a real user gesture, not just a permission grant.
- Save validation — the editor checks its internal state, not the DOM, when deciding whether content exists. A populated DOM with empty internal state triggers the "Something is wrong" error.
What would work
The only approach that would reliably work is one that goes through Medium's editor's own transaction system. This means either:
- Full keyboard simulation — every keyDown/keyUp with all properties (key, code, keyCode, modifiers, location, text), with realistic timing. This is slow but should work because it's indistinguishable from real typing at the event level.
- Medium's API — Medium has (had?) a REST API for article creation. It was deprecated in 2024. If it still works, it's the cleanest path.
-
Direct ProseMirror transaction injection — find the editor's ProseMirror instance in the JS context and call
editor.view.dispatch(editor.view.state.tr.insertText(...)). This requires knowing Medium's internal variable names, which are minified and change between deployments.
The real lesson
Browser automation for rich text editors is fundamentally different from automating forms. Forms use standard input elements with well-known APIs (value, dispatchEvent). Rich text editors use contenteditable divs with custom state management. The DOM is a view, not the source of truth. Setting the view doesn't update the model.
If you need to automate content publishing, check for an API first. If there's no API, check if the editor is open source (ProseMirror, TipTap, Slate — all have documented transaction APIs). If it's a proprietary editor like Medium's, full keyboard simulation is your only reliable option, and it's slow.
I published the article on GitHub instead. It took 30 seconds.
Top comments (0)