there are only three ways to publish a blog post automatically
i wanted one thing: write a post once, and have it show up on my own site, on dev.to, on hashnode, on medium, on substack, without doing it six times by hand.
it took a day. most of that day was not writing code. it was finding out which of those platforms can actually be automated, and discovering that the most popular open source tool for this is quietly broken.
TL;DR
- there are only three mechanisms. an official API, a commit to the repo your site builds from, or driving your logged-in browser session. every tool is one of these three.
- medium and substack have no publishing API. medium closed its to new signups in 2023. substack never had one.
- tools that claim to post to medium are usually driving a browser session with your cookies, or using a token issued before 2023. the best-starred one on github does the second and does not mention it.
- the "import" button beats the API you wish existed. medium's importer sets the SEO tag for you. pasting does not.
- order matters more than automation. publish on your own site first, or your copy can outrank your original.
the three mechanisms
this is the whole mental model. once you have it, every tool in this space becomes obvious.
1. an official API. the platform gives you a key, you send it a post, it publishes. boring and durable. dev.to, hashnode, ghost and wordpress all work this way.
2. a commit to your own repo. if your site is built from files in a git repository, which most modern personal sites are, then "publishing" is just adding a file. no API required, because it is your repo.
3. driving a logged-in session. a program opens a browser, uses the cookies from when you logged in, and clicks the buttons a human would. this is what tools resort to when there is no API.
the third one is where things go wrong, and it is worth knowing why. it breaks whenever the platform changes a button. it very likely violates their terms of service. and when it breaks badly, it breaks your account, not the tool author's. you are the one who gets suspended.
so the rule i settled on: if a platform has no API, i do not automate it. i produce something a human pastes or imports.
the part that surprised me
medium's API has been closed to new users since 2023.
not deprecated with a migration path. closed. existing tokens kept working, no new ones are issued. when it happened it broke buffer, zapier and ifttt, and one small publishing company wrote a public post about ending their medium integration over it.
substack never shipped one at all. it launched a developer API in 2026, but it searches for creator profiles. it cannot post.
now the uncomfortable bit. i went looking at the open source tools for this, because i would rather adopt something than build it. the most-starred project i found publishes to medium using an "integration token", and its documentation does not mention the shutdown anywhere. if you set it up today, you would generate a token that cannot be generated, and find out something is wrong later.
a much smaller project, five stars, handles it honestly: for medium it returns a message saying "this needs a browser" and hands you a link. no credentials, no pretending.
popularity is not the signal here. the small honest one is the one i would trust.
what actually works for medium
here is the thing i did not expect: the feature medium does support is better than the API i was hoping for.
go to medium.com/p/import, paste the URL of your published post, and medium fetches it, creates a draft, and automatically adds the tag that tells google your site is the original. it also backdates the post to your original publish date.
that tag is called a canonical link, and it is the whole ballgame for cross-posting. without it, google sees two copies of the same article and picks one. medium is a large trusted domain and your blog probably is not, so it usually picks medium. you can lose your own search traffic to your own copy.
if you paste into medium's editor instead, you get no canonical tag automatically. you have to remember to set it by hand, in a settings menu, every time.
i had built my own tool to generate a paste-ready body with "remember to set the canonical link" as step four in a checklist. that was wrong. a step a human has to remember is a step that eventually gets skipped, and this is the one step where skipping it costs you the thing you were trying to gain. so i changed it to lead with the importer.
what actually works for substack
substack has an importer too, and it takes an RSS feed.
RSS is the old, boring, still-working format that blogs use to publish a machine readable list of their posts. if your site has one, substack can pull from it.
mine did not. i checked four common addresses and every one was a 404. so the blocker for substack was never substack. it was that my own site had no feed.
that took about forty lines of code to fix, and there were two details that matter if you do the same:
- put the full post in the feed, not a summary. an importer given a summary imports a summary.
- make every link and image address absolute. a feed gets read on someone else's servers. a link like
/writing/my-post/chart.pngmeans "on whatever site is asking", so images silently vanish from the imported copy. mine had to be rewritten to the fullhttps://rcmisk.com/...form.
the order that protects you
this is the part i would tell someone who takes nothing else from this post.
publish on your own site first. wait for it to actually be live. then syndicate.
not for tidiness. a canonical link has to point at a page that exists. if you publish everywhere at once, for some window of time the only live version is the copy, and the copy is what gets indexed and credited.
google's own guidance now goes further and suggests the syndicated copy should be hidden from search entirely, because canonical tags have repeatedly failed to do their job. medium does not let you hide a post from search, so the canonical link is the only lever you have there, and it is a mitigation rather than a guarantee.
so the sequence is: own site, live, indexed, then everywhere else.
setting it up
for each channel, here is what you actually need.
your own site. if it builds from a git repo, you need a way to commit a file to it. i used a github app scoped to that one repository, which mints a token that expires in an hour and can touch nothing else. it opens a pull request and never merges it, so publishing stays a decision i make.
dev.to. settings, extensions, generate an API key. one key, one header. note it caps you at four tags and silently drops the rest.
hashnode. settings, developer, personal access token. you also need your publication id, which is a hash rather than your username. one warning that cost me time even reading the official schema: tags have to be objects, not plain strings. send strings and the API accepts your post, publishes it, and silently discards every tag.
ghost. settings, integrations, add a custom integration. the key comes as id:secret and you need both halves. ghost also wants HTML rather than markdown.
medium. nothing to set up. use the importer.
substack. nothing to set up, but your site needs an RSS feed for its importer to read.
what is still manual, on purpose
three things, and i left all three deliberately.
merging the post on my own site. the tool opens a pull request. i merge it. that is the last point where i can read the thing before it is public.
publishing live on the API platforms. by default my tool leaves a draft on dev.to and hashnode. a draft is recoverable. a live post on someone else's platform, in front of their audience, is not.
medium and substack entirely. i paste or import. that is fine. it is two minutes, and it is two minutes i would rather spend than hand a machine my session cookies.
the honest summary
the automation was the easy part. the useful part was learning the shape of the problem:
- three mechanisms, and one of them you should refuse to use.
- two of the biggest platforms cannot be automated, and the tools claiming otherwise are worth reading closely before you trust them.
- the button they do give you is better than the API you wanted.
- and the order you publish in matters more than any of it.
this post went out through the thing it describes. it was committed to my site as a pull request i merged myself, and it will reach the other channels only after this page has been live long enough to be the original.
sources
- Medium: importing a post and set a canonical link backed: the importer sets rel=canonical automatically and backdates; pasting does not.
- write.as on ending their Medium integration backed: Medium cut third-party publishing access without notice, breaking existing integrations.
- Substack: importing from another platform backed: the importer accepts an RSS feed, which is the supported route for a custom-domain site.
- dev.to API backed: POST /api/articles, the api-key header, and the four-tag cap.
- Hashnode's own GraphQL schema backed: publishPost is a single mutation, tags are objects with a required slug, and originalArticleURL is the canonical field. A widely shared community guide describes a different two-step flow; the schema is what the server runs.
- Ghost Admin API backed: the JWT shape and that it takes HTML rather than markdown.
- Google on cross-domain canonicals backed: canonical tags have repeatedly failed for syndicated copies, and noindex is now preferred.
- aws-community-projects/blog-crossposting-automation backed: the most-starred tool in this space publishes to Medium via an integration token with no mention of the 2023 shutdown. content-distribution-mcp backed: the smaller project that returns "needs a browser" instead.