From ad39448e2f1dc655b8695f43135343c84d77f674 Mon Sep 17 00:00:00 2001 From: Tim Wu Date: Sun, 21 Jun 2026 14:08:33 +0800 Subject: [PATCH] Add Medium publishing domain skill Document publishing a Medium story via the classic graf editor, complementing the read-oriented scraping.md / article-hydration.md: - URLs (new-story -> /p//edit -> /submission -> canonical) - graf block structure and HTML -> graf paste mapping - the synthetic-paste mechanic; title handling (first

-> graf--title) - multi-line code = one
 per line (newlines collapse)
- tag commit via JS focus + char-less trusted Enter
- traps: no API, new-draft throttling, execCommand-clear corrupts save,
  beforeunload freeze, Cmd+A not registering, retina DPR

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .../domain-skills/medium/publishing.md        | 94 +++++++++++++++++++
 1 file changed, 94 insertions(+)
 create mode 100644 agent-workspace/domain-skills/medium/publishing.md

diff --git a/agent-workspace/domain-skills/medium/publishing.md b/agent-workspace/domain-skills/medium/publishing.md
new file mode 100644
index 00000000..db915ca6
--- /dev/null
+++ b/agent-workspace/domain-skills/medium/publishing.md
@@ -0,0 +1,94 @@
+# Medium — Publishing a Story
+
+Composing and publishing a Medium story by driving the editor. For *reading* Medium, see `scraping.md` (APIs) and `article-hydration.md` (DOM extraction) — this file is the write path.
+
+There is **no public API for publishing** (the legacy Medium API stopped issuing integration tokens), so the editor must be browser-driven.
+
+## URLs
+
+- `https://medium.com/new-story` — creates a draft; once it autosaves it redirects to `/p//edit`. **If it never redirects, the draft isn't saving** (see throttle trap).
+- `https://medium.com/p//edit` — the editor for one draft.
+- `https://medium.com/p//submission?...` — the publish panel (preview card, topics/tags, Publish button).
+- `https://medium.com/me/stories/drafts` — drafts list; each row's hover "Toggle actions" (kebab) menu has **Delete story**.
+- Published canonical: `https://medium.com/@/-` (short form `https://medium.com/p/`).
+
+## Editor structure (classic "graf" editor)
+
+One big contenteditable: `div.js-postField` (class `postArticle-content js-postField … editable`, `role=textbox`, `data-default-value="Title\nTell your story…"`). Blocks are `.graf` elements whose class names carry the type:
+
+| graf class | meaning |
+| :-- | :-- |
+| `graf--title` | the story title — the FIRST block (an `h3` styled as title) |
+| `graf--h3` | big heading |
+| `graf--h4` | small heading |
+| `graf--p` | paragraph |
+| `graf--li` | list item |
+| `graf--pre graf--preV2` | code block (syntax-highlighted) |
+| `graf--blockquote` | quote |
+
+Inline runs: ``, ``, and **`` (inline code is preserved as monospace)**.
+
+## The key mechanic: inject content via a synthetic paste
+
+Medium converts pasted HTML into `.graf` blocks. Focus the contenteditable, then dispatch a `paste` `ClipboardEvent` with a `DataTransfer` holding `text/html` (run via `js(...)`):
+
+```js
+var ed  = document.querySelector('.js-postField,[contenteditable="true"]');
+var tgt = document.activeElement && document.activeElement.isContentEditable ? document.activeElement : ed;
+var dt = new DataTransfer();
+dt.setData('text/html', htmlString);
+dt.setData('text/plain', plainString);
+tgt.dispatchEvent(new ClipboardEvent('paste', {clipboardData: dt, bubbles:true, cancelable:true}));
+```
+
+HTML → graf mapping (verified):
+
+- **First `

` of a paste into a *fresh* editor → `graf--title`** (the title). Subsequent `

/

/

` → `graf--h3` (big heading); `

` → `graf--h4` (small heading). +- `

` with `//` → paragraph with inline runs. +- `

  • ` → bullet list; `
    ` → blockquote. +- `
    ` → code block (`graf--pre`). **Newlines inside one `
    ` are collapsed** — and so are `
    ` and `
    `. For multi-line code, emit **one `
    ` per line** (consecutive `
    ` stay as separate code blocks).
    +
    +Converting markdown for this editor: `##`→`

    `, `###`→`

    `, inline code `` `x` ``→``, `**x**`→``, tables → bullet list (`
  • cmd — desc
  • `), code fences → one `
    ` per line.
    +
    +## Setting the title reliably
    +
    +The title is finicky. Two working options:
    +
    +1. **Prepend `

    {title}

    `** to the body HTML and paste into a *fresh* editor with the caret at the top — the first `

    ` becomes `graf--title`. (If you click into the body first, that `

    ` becomes a body heading instead and the title stays empty.) +2. **Place the caret in the title block via JS, then `type_text`:** + ```js + var t = document.querySelector('.graf--title'); + var ed = document.querySelector('.js-postField'); ed.focus(); + var r = document.createRange(); r.selectNodeContents(t); r.collapse(true); + var s = getSelection(); s.removeAllRanges(); s.addRange(r); + ``` + Clicking the empty title block does **not** focus it (it has near-zero height when empty). If this leaves a duplicate heading, select that block's contents with a JS range and delete it with **real** `Backspace` key events (not `execCommand`). + +## Adding tags (publish panel) + +Topic input is `input[placeholder^="Add a topic"]` (becomes `Add more topics…` after the first tag). Commit each tag with **JS `.focus()` + a char-less trusted Enter**: + +```python +js("document.querySelector('input[placeholder^=\"Add\"]').focus()") +cdp("Input.dispatchKeyEvent", type="keyDown", windowsVirtualKeyCode=13, key="Enter", code="Enter") +cdp("Input.dispatchKeyEvent", type="keyUp", windowsVirtualKeyCode=13, key="Enter", code="Enter") +``` + +`press_key("Enter")` does **not** commit (its synthetic `char` event defeats it), and **comma is rejected** ("Tags only support letters, numbers, spaces and dashes"). Up to 5 tags. + +## Publish flow + +1. Click the top-bar **Publish** → the submission page opens. +2. Preview title/subtitle auto-populate from the story title; add tags. +3. Click the **Publish** ("Publish now") button at the bottom of the panel. +4. Redirects to `/p/?postPublishedType=initial`; read `link[rel="canonical"]` for the public URL. + +## Traps + +- **No publishing API** — browser-drive only. +- **New-draft creation throttles.** After creating several drafts quickly, `/new-story` stops redirecting to `/p//edit` and won't save (no error, just no persistence). **Editing an existing draft still saves fine** — reuse a known-good draft, or space out creation. +- **`execCommand('delete')` to clear the editor corrupts Medium's save model** → red banner "Something is wrong and we cannot save your story." Don't clear that way. A single clean paste into a fresh/empty editor saves fine; for edits use real key events. +- **`Cmd+A` select-all doesn't register via CDP** (even with `commands:["selectAll"]`), same as other rich editors. +- **`beforeunload` freezes navigation** when there are unsaved changes — `Page.navigate`/`goto_url` times out. Recover with `cdp("Page.handleJavaScriptDialog", accept=True)`, then continue. Opening a fresh tab avoids the dialog entirely. +- The per-code-block **"Auto (C#)" language label is an in-editor hover artifact** — the published view is clean. +- **Retina DPR=2**: read coordinates from `getBoundingClientRect`, never off a screenshot (`click_at_xy` takes CSS pixels).