You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
devto-post: document programmatic cover-image upload (upload_file against
the "Upload Cover Image" button) and canonical-URL entry via Advanced Post
Options; capture tag-aliasing and re-edit-via-/edit gotchas.
medium-post: absolutize relative body image URLs during extraction (Medium
silently drops relative srcs, dropping in-body screenshots); rework Step 3
verification to re-open the draft and count images rather than trusting the
/new-story URL; add canonical-link reminder and safe flawed-draft deletion.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: .claude/skills/devto-post/SKILL.md
+31-5Lines changed: 31 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,10 +18,13 @@ Automate posting a blog post to dev.to using the Chrome DevTools MCP. Opens the
18
18
19
19
Read the blog post markdown file to extract:
20
20
-`title` — from frontmatter
21
+
-`slug` — from frontmatter (used to build the canonical URL)
21
22
-`categories` — from frontmatter (map to dev.to tags, max 4)
22
23
-`coverImage` — from frontmatter (absolute URL)
23
24
- Body content — everything after the closing `---`
24
25
26
+
Also resolve the **local path of the cover image** for upload. The `publicDir` override means local assets live under `libs/portfolio/shared/`, so map the `coverImage` URL by stripping the host: `https://dalenguyen.me/assets/...` → `libs/portfolio/shared/assets/...`. Upload the `.png` (a `.webp` sibling also exists — dev.to wants the PNG).
Take a snapshot to confirm the page loaded and the user is logged in.
40
43
41
-
### Step 4: Fill in the title
44
+
### Step 4: Upload the cover image
45
+
46
+
Use `mcp__chrome-devtools__upload_file` with the **uid of the "Upload Cover Image" button** (it acts as the file chooser) and the local PNG path resolved in Step 1:
After upload, the button row changes to **Change / Generate Image / Cover Video Link / Remove** and a thumbnail appears — take a screenshot to confirm. dev.to re-hosts the image on its own S3 CDN.
53
+
54
+
### Step 5: Fill in the title
42
55
43
56
Click the "Post Title" textbox uid from the snapshot and type the title.
44
57
45
-
### Step 5: Fill in the tags
58
+
### Step 6: Fill in the tags
46
59
47
60
Dev.to supports up to 4 tags. Confirm each tag by appending a **trailing comma** — do NOT use `Enter` or `press_key`, it does not work and causes tags to concatenate instead of being confirmed separately.
48
61
@@ -56,7 +69,9 @@ type_text "vite,"
56
69
57
70
**Do NOT use `press_key Enter`** — it does not trigger tag confirmation and the text accumulates as one long string. The trailing comma is the only reliable way to confirm each tag.
58
71
59
-
### Step 6: Fill in the content
72
+
dev.to may alias a tag to a canonical equivalent (e.g. `accessibility` displays as `a11y`) — this is expected, not an error.
73
+
74
+
### Step 7: Fill in the content
60
75
61
76
Use `mcp__chrome-devtools__evaluate_script` to inject the prepared content directly into the textarea via a native input setter, then dispatch `input` and `change` events so the editor picks it up:
62
77
@@ -74,7 +89,17 @@ Use `mcp__chrome-devtools__evaluate_script` to inject the prepared content direc
74
89
75
90
Take a screenshot after to verify content appeared in the editor.
76
91
77
-
### Step 7: Save as draft
92
+
### Step 8: Set the canonical URL
93
+
94
+
Click the **"Advanced Post options"** button (gear, at the bottom of the editor) to open the Advanced Post Options modal. Click the **"Canonical URL"** textbox and type the original post URL:
95
+
96
+
```
97
+
https://dalenguyen.me/blog/<slug>
98
+
```
99
+
100
+
Then click **"Done"** to close the modal. This points search engines at your blog as the original source, avoiding duplicate-content penalties. After saving, the published byline reads "Originally published at dalenguyen.me" — confirmation it took.
101
+
102
+
### Step 9: Save as draft
78
103
79
104
Click the **"Save Draft"** button — do NOT click Publish. The user must review the rendered draft before publishing.
80
105
@@ -85,4 +110,5 @@ Take a final screenshot confirming the draft was saved (dev.to shows an "Unpubli
85
110
- Always save as draft, never publish directly
86
111
- After saving, dev.to redirects to the unpublished post preview — confirm the URL changed away from `/new`
87
112
- Tags are confirmed by a trailing comma (e.g. `type_text "ai,"`) — `press_key Enter` does not work and causes tags to concatenate
88
-
- The cover image is not set programmatically — inform the user they can upload it manually from the draft editor if needed
113
+
- The cover image **is** uploaded programmatically via `upload_file` against the "Upload Cover Image" button (Step 4); the local PNG lives under `libs/portfolio/shared/assets/...`
114
+
- To re-edit an already-saved draft (e.g. to add the cover or canonical after the fact), navigate directly to `<post-url>/edit` — `navigate_page back` can land on `about:blank` instead of the editor
var hs = clone.querySelectorAll('h1,h2,h3,h4,h5,h6');
70
80
for (var i = 0; i < hs.length; i++) hs[i].removeAttribute('id');
@@ -127,18 +137,28 @@ context.browser.close()
127
137
128
138
### Step 3: Confirm and report to user
129
139
130
-
After the script runs:
131
-
- Confirm the URL is `/p/<id>/edit` (not `/new-story`)
132
-
- Tell the user the draft URL
133
-
- List the topics to add manually from the publish panel
140
+
After the script runs, **verify by re-opening the draft — do not trust the printed URL.** The script reads `page.url` a few seconds after paste, and Medium often still shows `/new-story` then (it redirects to `/p/<id>/edit` only after the first auto-save). A `/new-story` print is NOT proof of failure.
141
+
142
+
To verify, open `https://medium.com/me/stories/drafts`, find the draft by title, then open `https://medium.com/p/<id>/edit` and count content in the `article`:
143
+
-**Images** = 1 (cover) + every in-body image. If you only see 1, the body images were dropped — check that Fix 1 (absolute URLs) ran.
144
+
- Headings, code blocks, paragraphs, and char count are non-zero and the body runs intro-to-conclusion.
145
+
146
+
Then report to the user:
147
+
- The real draft URL (`/p/<id>/edit`)
148
+
- The topics to add manually from the publish panel
149
+
- The **canonical link** to set (publish panel → "Advanced settings → Customize canonical link"): `https://dalenguyen.me/blog/<slug>` — points SEO at the original blog post
150
+
- That **tables won't render** on Medium (see Notes) if the post has any
134
151
- Remind them to click Publish after reviewing
135
152
153
+
If you created a corrected replacement draft, **delete the flawed one** so the user can't publish it by mistake: on the drafts page each row has a hover-hidden "Toggle actions menu" button — click it via JS (`el.click()`, not `page.click`, which fails on the hidden element), choose "Delete story", then click "Delete" in the confirmation dialog (scope the query to `[role="dialog"]` so you don't hit a stray button). Create the replacement first, then delete — Medium draft deletion is permanent.
154
+
136
155
## Notes
137
156
138
157
-**Always save as draft** — do not click Publish
139
158
- Medium auto-saves continuously; no explicit save button needed
140
159
-**Topics** must be added manually from the publish panel (click "Publish" → add topics)
141
-
-**Tables** in the blog post will not render in Medium (Medium doesn't support tables) — leave as-is
160
+
-**Tables** in the blog post will not render in Medium (Medium doesn't support tables) — leave as-is, or tell the user to paste table data as screenshots
161
+
-**Relative image URLs are silently dropped.** Blog body images often use relative srcs (`assets/images/blog/x.png`); Medium only imports absolute (`https://…`) URLs. Step 2's extraction rewrites them — without it, only the cover (already absolute) survives and before/after screenshots vanish. Always confirm the final image count matches expectations
142
162
- The `<figure><figcaption>` pattern is the correct way to embed cover image + caption in one paste — `<img><br>` causes the following text to be misinterpreted as the caption
-**`DataTransfer` + `ClipboardEvent` does NOT work** in Medium's editor — the paste event fires but Medium ignores it. Use `navigator.clipboard.write()` with `ClipboardItem` + a real `Meta+v` keystroke instead
0 commit comments