Use when a finished article has to land on Medium correctly — profile vs publication, cross-posting under a canonical link so your own site keeps the SEO credit, import-tool failures, the 5-tag cap, pseudo-Markdown gotchas. NOT writing or headlining the piece (that is `medium-writing`), NOT cadence or earnings (that is `medium-strategy`).
npx skills add https://github.com/ericrisco/rsc-harness --skill medium-publishing
Land a finished article on Medium with correct metadata: right destination, no duplicate-content penalty, valid tags, intact formatting. Writing the article is ../medium-writing/SKILL.md; deciding when/where/how-often to publish and how it grows is ../medium-strategy/SKILL.md. This skill is only the mechanics of getting bytes onto the platform with the right metadata attached.
The Medium API is closed to new integrations. As of 2025-01-01 Medium issues no new integration tokens and accepts no new integrations; only tokens minted before that date still work. Why it matters: any plan that starts with "register a Medium API token and automate it" is dead on arrival for anyone who didn't already have one. Integration platforms confirm this — the Medium app in Make is marked legacy, and n8n's Medium credentials can no longer be newly configured.
So the default publishing path is web editor + import tool + canonical link, done through the UI. Treat the API as a legacy escape hatch (last section), not the plan — when asked "can I get a token and automate this?", the honest answer is almost always no, so say why and route to the import path below rather than promising automation.
Branch on where the article already lives and what you control:
| Situation | Path | Canonical handling |
|---|---|---|
| Article only ever lives on Medium | Write or paste natively in the web editor, publish | None needed — Medium *is* the original |
| Article already published on your own site/blog | Import tool: paste the original URL | Auto-set to that URL + auto-backdated |
| Import tool fails (403/404/timeout) | Native paste, then set canonical by hand | Manual: Story settings -> Customize canonical link |
| You hold a pre-2025 API token and want automation | Legacy POST /v1/users/{id}/posts | canonicalUrl field in the request body |
| Destination is a publication you write for | Submit draft to the publication | Same as above; submission is separate from canonical |
When the article already lives on your own site, you must tell search engines that the origin — your page, never the Medium URL — is canonical, or Google sees two copies and may credit Medium or neither. Two mechanisms, in order of preference:
1. Import tool (preferred). Pasting the original URL into Medium's import field does three things at once:
article:published_time meta tag.Path: New story -> import icon (or medium.com/p/import) -> paste the original URL -> Import. Review the draft, then publish.
2. Manual canonical (fallback). If you drafted natively or the import failed, set it per story: More options (•••) -> Story settings -> advanced/edit -> "Customize canonical link", paste the origin URL, then publish. This is per-story; there is no global default.
Bad: Open a new Medium story, paste the article body, hit Publish.
-> Duplicate content, no canonical, wrong date. Your own page now
competes with Medium for its own ranking.
Good: Use the import tool (canonical + backdate set automatically), or if
you paste natively, set "Customize canonical link" to the origin URL
BEFORE publishing.
The importer is an HTTP crawler hitting your URL; failures are crawl failures, not Medium bugs. Map the symptom to the cause:
| Symptom | Likely cause | Fix |
|---|---|---|
| 400 / 404 | URL unreachable, redirected, or page metadata missing | Verify the URL loads anonymously; ensure <link rel="canonical"> and article:published_time exist |
| 403 | Bot-blocking (WAF, Cloudflare challenge, login wall) | Serve a clean static copy the crawler can reach, or paste natively + set canonical manually |
| 500 / 504 | Crawl timeout — heavy JS, slow page | Provide a lightweight static HTML version of the article |
| Imports but body is empty/garbled | Content rendered client-side only (JS) | Same: static HTML with the content in the initial markup |
The reliable workaround for stubborn pages: publish a minimal static HTML page of the article carrying <link rel="canonical"> and <meta property="article:published_time">, import *that*, then you can take it down. Full recipe and the complete error -> cause -> fix matrix are in references/cross-post-and-canonical.md.
Five tags, exactly. Medium allows up to 5 tags per story; they drive topic distribution and discovery, so they are functional metadata, not decoration. Pick 5 real topics readers browse, not keyword-stuffed variants.
Bad: tags: react, reactjs, react.js, react-hooks, javascript, frontend, webdev (7, redundant)
Good: tags: react, javascript, web-development, frontend, programming (5, distinct surfaces)
Medium is NOT a full Markdown editor. The web editor interprets a *subset* of Markdown shortcuts as you type, and silently ignores the rest. Know the gotchas:
#, ##), blockquote (>), emphasis (*/_), ordered/bulleted lists (1., *), and the --- separator work as you type.| col | col | table renders as plain text. Restructure into a list, or insert the table as an image.Cmd/Ctrl + Option + 6. Do not rely on indentation-based code.Bad: Paste a raw Markdown table and assume it renders as a table.
Good: Convert the table to a labeled list, or screenshot/export it as an
image and insert the image. Reserve real tables for the original site.
(The legacy API's contentFormat: markdown is more permissive than the web editor — but that path is closed to new users.)
Two outcomes, often confused:
Do not attempt to register a new token; it will be refused. This applies *only* to tokens minted before 2025-01-01.
# 1. Resolve your user id
curl -s -H "Authorization: Bearer $MEDIUM_TOKEN" \
https://api.medium.com/v1/me
# -> { "data": { "id": "<userId>", ... } }
# 2. Create a post with canonical + publish status
curl -s -X POST \
-H "Authorization: Bearer $MEDIUM_TOKEN" \
-H "Content-Type: application/json" \
https://api.medium.com/v1/users/<userId>/posts \
-d '{
"title": "My title",
"contentFormat": "markdown",
"content": "# My title\n\nBody...",
"canonicalUrl": "https://mysite.com/original-post",
"tags": ["react", "javascript", "web-development"],
"publishStatus": "draft"
}'
publishStatus is public | draft | unlisted. For a publication, POST to /v1/publications/{publicationId}/posts; a draft there stays pending an editor. Full field reference and response shapes are in references/legacy-api.md.
| Anti-pattern | Why it's wrong | Do instead |
|---|---|---|
| Paste a copy of your own article, publish with no canonical | Duplicate content; your site loses ranking credit | Import tool, or set canonical to the origin before publishing |
| Plan an automated pipeline on a freshly registered API token | No new tokens since 2025-01-01; it will never authorize | Use the import tool / web editor; reserve API only for pre-2025 tokens |
| Cram 6+ tags or keyword-stuff the tag slots | Max is 5; redundant tags waste distribution surface | Pick 5 distinct topics readers actually browse |
| Paste a raw Markdown table and assume it renders | The web editor has no table support | List or image; keep the table on the origin site |
| Submit to a publication and assume it goes live | Submission is pending until an editor acts; needs writer access | Self-publish for instant, or submit only as an accepted writer |
| Set canonical to the Medium URL | Points credit at Medium, defeating the cross-post | Canonical must point to your origin site |
| Native paste of a backdated piece without checking the date | Manual paste does not backdate; canonical/date mismatch | Use import (auto-backdate) or set the date deliberately |
A correctly cross-posted article passes all of these:
Take ericrisco/medium-publishing from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.