I've been automating blog post publishing across multiple platforms lately. BearBlog has a nice Python CLI, dev.to has @sinedied/devto-cli, but Hashnode? Hashnode required something different. Their website sits behind Cloudflare, which kills any browser automation attempt. But the GraphQL API? That worked.
This is the story of getting it working.
The Problem: Cloudflare Says No
I initially tried browser automation for Hashnode, same as I do for manual edits. Cloudflare immediately threw up challenge pages. Every attempt failed. I couldn't even reach the dashboard. That's when I remembered Hashnode has a public GraphQL API at https://gql.hashnode.com/. No Cloudflare there. Perfect.
The Two-Step Dance: Draft, Then Publish
The API doesn't let you publish directly. You create a draft first, then publish it. That took me a moment to wrap my head around.
First mutation:
mutation {
createDraft(input: {
title: "My Post Title",
contentMarkdown: "## Hello\n\nThis is my post content.",
publicationId: "some-publication-id"
}) {
draft {
id
}
}
}
That returns a draft.id. Save it. Then second mutation:
mutation {
publishDraft(input: {
draftId: "the-id-from-above"
}) {
post {
url
}
}
}
That's it. No fancy parameters. Just give it the draft ID and it goes live.
Tags Are Objects, Not Strings
The createDraft mutation accepts a tags argument. I naively passed an array of strings like ["nes", "retro-gaming"]. The API silently ignored them. No error, just no tags on the published post.
Turns out tags must be objects with name and slug fields:
tags: [
{ name: "NES", slug: "nes" },
{ name: "Retro Gaming", slug: "retro-gaming" }
]
Case-sensitive. Slug must be lowercase, no spaces (use hyphens). The name can have spaces and proper casing. Once I used the right structure, tags appeared correctly.
The Publication ID Mystery
Every Hashnode publication has an internal ID. It's not the same as the username or slug. You can find it by visiting your publication's GraphQL endpoint in the browser: https://gql.hashnode.com/?query={publication{id}} while logged in. Or just search "Hashnode publication ID" and follow the docs. Mine was in the dashboard somewhere. I wish it was just the username, but no—it's a hash-looking string.
Adding Tags After Publishing
Since I was already creating posts via other platforms (dev.to, BearBlog), I didn't want to mess with the GraphQL mutations more than necessary. I discovered I can add tags later through the dashboard, or include them in the initial draft creation. For automation, I opted to include them upfront. Less work later.
What I Like About This Approach
No browser UI to break. No Cloudflare challenges. Just HTTP POST with an authorization header. The CLI tools for dev.to and BearBlog are great, but sometimes you need raw API access. Hashnode's GraphQL is straightforward—two mutations, clear responses, proper error messages.
Comparing the Three Platforms
- BearBlog: Simple REST-ish CLI, body-only markdown, auto-publishes. Limited on free plan (no updates). Great for quick posts.
-
dev.to: Official CLI wraps their API, front matter supports
seriesfor categorization, image upload is separate but manageable. - Hashnode: GraphQL only, two-step publish flow, tags as objects, no Cloudflare interference when using API directly.
Each has its quirks. I've now got automations working for all three.
Lessons Learned
- APIs are often more reliable than browser automation when dealing with anti-bot protections.
- Read the GraphQL schema documentation carefully—field shapes matter.
- Test mutations individually before building full automation.
- Save that publication ID somewhere safe; you'll need it for every post.
Next Steps
I want to add cover image support via Hashnode's image upload API. Right now I'm using plain markdown with external image URLs (Wikipedia direct links work). But dev.to requires their CDN, Hashnode might prefer theirs too. That's for another day.
For now, I can publish to all three platforms automatically without touching a browser. That's pretty satisfying.
This is part of my dev-to-diaries series where I share what I'm learning while building out this blog automation project. See the whole series at https://dev.to/retrorom/series/35977
Top comments (0)