Skip to content

Commit 9287fd2

Browse files
dalenguyenclaude
andauthored
docs(skills): add cover image + canonical URL to devto/medium posting skills (#175)
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>
1 parent 01e3b42 commit 9287fd2

2 files changed

Lines changed: 56 additions & 10 deletions

File tree

.claude/skills/devto-post/SKILL.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ Automate posting a blog post to dev.to using the Chrome DevTools MCP. Opens the
1818

1919
Read the blog post markdown file to extract:
2020
- `title` — from frontmatter
21+
- `slug` — from frontmatter (used to build the canonical URL)
2122
- `categories` — from frontmatter (map to dev.to tags, max 4)
2223
- `coverImage` — from frontmatter (absolute URL)
2324
- Body content — everything after the closing `---`
2425

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).
27+
2528
### Step 2: Prepare dev.to content
2629

2730
Transform the body before pasting:
@@ -38,11 +41,21 @@ mcp__chrome-devtools__new_page url="https://dev.to/new"
3841

3942
Take a snapshot to confirm the page loaded and the user is logged in.
4043

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:
47+
48+
```
49+
mcp__chrome-devtools__upload_file uid=<Upload Cover Image button uid> filePath="/abs/path/libs/portfolio/shared/assets/images/blog/<slug-image>.png"
50+
```
51+
52+
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
4255

4356
Click the "Post Title" textbox uid from the snapshot and type the title.
4457

45-
### Step 5: Fill in the tags
58+
### Step 6: Fill in the tags
4659

4760
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.
4861

@@ -56,7 +69,9 @@ type_text "vite,"
5669

5770
**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.
5871

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
6075

6176
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:
6277

@@ -74,7 +89,17 @@ Use `mcp__chrome-devtools__evaluate_script` to inject the prepared content direc
7489

7590
Take a screenshot after to verify content appeared in the editor.
7691

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
78103

79104
Click the **"Save Draft"** button — do NOT click Publish. The user must review the rendered draft before publishing.
80105

@@ -85,4 +110,5 @@ Take a final screenshot confirming the draft was saved (dev.to shows an "Unpubli
85110
- Always save as draft, never publish directly
86111
- After saving, dev.to redirects to the unpublished post preview — confirm the URL changed away from `/new`
87112
- 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

.claude/skills/medium-post/SKILL.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ html_body = blog.evaluate("""() => {
6565
spans[i].replaceWith(document.createTextNode(spans[i].textContent));
6666
}
6767
68+
// Make relative body image URLs absolute — Medium silently DROPS relative
69+
// srcs (e.g. assets/images/blog/x.png) and only imports absolute ones.
70+
var bodyImgs = clone.querySelectorAll('img');
71+
for (var i = 0; i < bodyImgs.length; i++) {
72+
var src = bodyImgs[i].getAttribute('src') || '';
73+
if (src && !/^https?:\\/\\//.test(src)) {
74+
bodyImgs[i].setAttribute('src', 'https://dalenguyen.me/' + src.replace(/^\\//, ''));
75+
}
76+
}
77+
6878
// Remove heading IDs (not needed on Medium)
6979
var hs = clone.querySelectorAll('h1,h2,h3,h4,h5,h6');
7080
for (var i = 0; i < hs.length; i++) hs[i].removeAttribute('id');
@@ -127,18 +137,28 @@ context.browser.close()
127137

128138
### Step 3: Confirm and report to user
129139

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
134151
- Remind them to click Publish after reviewing
135152

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+
136155
## Notes
137156

138157
- **Always save as draft** — do not click Publish
139158
- Medium auto-saves continuously; no explicit save button needed
140159
- **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
142162
- 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
143163
- CloakBrowser passes Medium's bot detection reliably
144164
- **`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

Comments
 (0)