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`).
Installs into .claude/skills of the current project.
Are you the author of Medium Publishing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/ericrisco-medium-publishing)
---
name: medium-publishing
description: "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`)."
tags: [medium, publishing, cross-posting, canonical-url, seo, import-tool]
recommends: [medium-writing, medium-strategy, seo-geo, social-publisher]
origin: risco
---
# 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.
## Reality check (2025-2026)
**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.
## Decision table — pick your path
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 |
## Cross-post without an SEO penalty (the core job)
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:
- imports the rendered content into a new draft,
- sets the story's canonical link to that original URL (search engines keep crediting your site),
- backdates the Medium post to your original publish date by reading the page's `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.
```text
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.
```
## When the import fails
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`.
## Tags & formatting at publish time
**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.
```text
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:
- Headings (`#`, `##`), blockquote (`>`), emphasis (`*`/`_`), ordered/bulleted lists (`1.`, `*`), and the `---` separator work as you type.
- **No Markdown tables.** Pasting a `| col | col |` table renders as plain text. Restructure into a list, or insert the table as an image.
- **Code blocks**: type triple-backtick then content, or `Cmd/Ctrl + Option + 6`. Do not rely on indentation-based code.
- **Embeds**: paste a bare URL on its own line (YouTube, gist, tweet) and Medium expands it. A URL inside a sentence stays a plain link.
```text
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.)
## Publications
Two outcomes, often confused:
- **Self-publish** puts the story on your own profile immediately.
- **Submit to a publication** routes the draft to that publication's editors, where it stays **pending** — not live — until one of them publishes it. A publication is editorially controlled, so submission is a request, not a publish action, and it only works if you are already an accepted writer there. If you're not, request access first; submitting otherwise goes nowhere.
## Legacy API — only if you hold a pre-2025 token
Do **not** attempt to register a new token; it will be refused. This applies *only* to tokens minted before 2025-01-01.
```bash
# 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-patterns
| 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 |
## Post-publish verify checklist
A correctly cross-posted article passes all of these:
- [ ] Canonical link present **and pointing to the origin** (your site), not the Medium URL.
- [ ] Publish date matches the original (backdated correctly if cross-posted).
- [ ] 5 tags or fewer, all distinct real topics.
- [ ] Code blocks render as code; embeds expanded; no broken Markdown tables.
- [ ] Correct destination — your profile, or the intended publication (and it actually went live, not stuck pending).