hashnode
GitHub通过 Hashnode GraphQL API 发布、更新和读取博客文章。支持创建草稿、公开投稿及列出出版物,需先验证 Token 并获取 Publication ID。
Trigger Scenarios
Install
npx skills add NeverSight/learn-skills.dev --skill hashnode -g -y
SKILL.md
Frontmatter
{
"name": "hashnode",
"license": "Apache-2.0",
"metadata": {
"author": "acedatacloud",
"version": "1.0"
},
"connections": [
"hashnode"
],
"description": "Publish, update and read blog posts on Hashnode via its GraphQL API. Use when the user mentions Hashnode, publishing a blog post to Hashnode, cross-posting an article to their Hashnode blog, saving a Hashnode draft, updating a published Hashnode post, or listing their Hashnode publications and posts.",
"when_to_use": "Trigger when the user wants to publish a Markdown article to their\nHashnode blog, save it as a draft, update a previously published\npost, or list \/ inspect their own Hashnode publications and posts.\nThe connector stores a Hashnode Personal Access Token with full\naccount access — confirm before publishing publicly (you can save a\ndraft first with the createDraft mutation).\n",
"allowed_tools": [
"Bash"
]
}
Call the Hashnode Public GraphQL API with curl + jq. The user's Personal
Access Token is in $HASHNODE_TOKEN. Single endpoint: https://gql.hashnode.com
(always POST a JSON body {query, variables}).
Every request needs these headers:
Authorization: $HASHNODE_TOKEN # raw token — NO "Bearer " prefix
Content-Type: application/json
GraphQL always returns HTTP 200; real failures live in the JSON errors
array — always inspect it and show it verbatim. An errors entry mentioning
Unauthorized / not authenticated means the token is invalid → the user must
re-connect the Hashnode connector.
Helper — send a query/mutation ($1 = query string, $2 = variables JSON):
gql() {
jq -n --arg q "$1" --argjson v "${2:-null}" '{query:$q, variables:$v}' \
| curl -sS -X POST https://gql.hashnode.com \
-H "Authorization: $HASHNODE_TOKEN" \
-H "Content-Type: application/json" \
-d @-
}
Always start by confirming the token and finding the publication id
publishPost / createDraft need a publicationId. Fetch the account and its
publications first (a user can have several blogs):
gql 'query { me { id username publications(first: 10) { edges { node { id title url } } } } }' \
| jq '{me: .data.me.username, publications: [.data.me.publications.edges[].node | {id, title, url}], errors: .errors}'
Pick the target blog's id (that is the publicationId). If the user has
exactly one publication, use it; otherwise ask which blog to post to.
Publish a post
Confirm with the user before publishing publicly. If they are not sure, save
a draft first (see below). tags is required — supply 1–5 tags, each as
{name, slug} (slug lowercase, no spaces).
PUB_ID="PUBLICATION_ID" # from the me query above
TITLE="My title"
BODY_MD="$(cat article.md)" # full Markdown body
VARS=$(jq -n --arg p "$PUB_ID" --arg t "$TITLE" --arg b "$BODY_MD" '{
input: {
publicationId: $p,
title: $t,
contentMarkdown: $b,
tags: [{name:"AI", slug:"ai"}, {name:"Web Development", slug:"web-development"}]
}
}')
gql 'mutation PublishPost($input: PublishPostInput!) {
publishPost(input: $input) { post { id slug url title } }
}' "$VARS" | jq '{post: .data.publishPost.post, errors: .errors}'
Useful optional input fields:
subtitle— post subtitle.slug— custom URL slug (otherwise derived from the title).canonicalUrl/originalArticleURL— when cross-posting, point SEO back to the original source. Set this whenever the same article also lives elsewhere.coverImageOptions: { coverImageURL: "https://..." }— cover image.publishedAt— ISO-8601 timestamp to backdate;disableCommentsetc. live undersettings.
Save a draft (safe, non-public)
Use this when the user has not explicitly approved public publishing:
VARS=$(jq -n --arg p "$PUB_ID" --arg t "$TITLE" --arg b "$BODY_MD" '{
input: { publicationId: $p, title: $t, contentMarkdown: $b }
}')
gql 'mutation CreateDraft($input: CreateDraftInput!) {
createDraft(input: $input) { draft { id slug title } }
}' "$VARS" | jq '{draft: .data.createDraft.draft, errors: .errors}'
Update a published post
updatePost needs the post id (from publishPost, or the list query below):
VARS=$(jq -n --arg id "POST_ID" --arg b "$(cat article.md)" '{
input: { id: $id, contentMarkdown: $b }
}')
gql 'mutation UpdatePost($input: UpdatePostInput!) {
updatePost(input: $input) { post { id url title } }
}' "$VARS" | jq '{post: .data.updatePost.post, errors: .errors}'
List / inspect my posts
gql 'query { me { posts(pageSize: 20, page: 1) { nodes { id title slug url views reactionCount responseCount publishedAt } } } }' \
| jq '[.data.me.posts.nodes[] | {id, title, url, views, reactions: .reactionCount, responses: .responseCount}]'
Single post with full content + stats:
gql 'query GetPost($id: ObjectId!) { post(id: $id) { title url views reactionCount responseCount content { markdown } } }' \
"$(jq -n --arg id "POST_ID" '{id:$id}')" | jq '.data.post | {title, url, views, reactions: .reactionCount}'
Gotchas
- No
Bearerprefix — theAuthorizationheader carries the raw token. publicationIdis mandatory for publishing/drafting — never guess it, read it from themequery.tagsis required onpublishPost— supply at least one{name, slug}; slug must be lowercase with hyphens (e.g.machine-learning).errorson HTTP 200 — GraphQL reports failures in theerrorsarray, not via status codes; always surface them.- Idempotency — re-running
publishPostcreates a new post each time; to change an existing one useupdatePostwith itsid.
Version History
- e0220ca Current 2026-07-05 22:54


