Hashnode (hashnode)
Hashnode is a GraphQL-only article provider. Rustiz sends a Personal Access
Token (PAT) and publishes with the publishPost mutation against
https://gql-beta.hashnode.com. There is no OAuth app, no scope selection and
no app review; Hashnode gates the API by publication plan, not by app
review.
- Rust source:
crates/rustiz-providers/src/hashnode.rs - Static tag catalogue:
crates/rustiz-providers/src/hashnode/tags.rs - Upstream port:
hashnode.provider.ts+hashnode.tags.ts - Editor: Markdown
- Connection mode:
customFields
Overview
| Property | Value |
|---|---|
| Identifier | hashnode |
| Display name | Hashnode |
| Connection mode | customFields (Personal Access Token) |
| Editor | markdown |
| Channel kind | social (article content) |
| Comments / threads | No. supports_comments = false; only the first PostDetails is published |
| Pending / async publishing | No. supports_pending = false; success is PostStatus::Completed |
| Analytics | No. supports_analytics = false |
| Post analytics | No. supports_post_analytics = false |
| Mentions | No. supports_mentions = false |
is_between_steps | false |
convert_to_jpeg | false |
max_concurrent_job | 3 |
Character limit (max_length) | 10000 characters for contentMarkdown |
| API | GraphQL, single POST / endpoint |
Server Environment Variables
required_env() returns no variables:
#![allow(unused)]
fn main() {
fn required_env(&self) -> &'static [&'static str] {
&[]
}
}
required_env_alternatives() is not overridden. The PAT belongs to the user,
not the deployment, so rustiz providers --status reports hashnode as
configured without any server configuration.
| Variable | Required | Default | Purpose |
|---|---|---|---|
HASHNODE_BASE_URL | No | https://gql-beta.hashnode.com | Overrides the GraphQL origin. The value is trimmed of a trailing slash. The override replaces the whole base, so it must include any path prefix. |
RESTRICT_UPLOAD_DOMAINS | No | unset | If set, main_image.path must contain this substring. |
NEXT_PUBLIC_BACKEND_URL | No | "" | Prefix used for a relative main_image.path. See the cover-image bug below. |
NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY | No | "" | Second part of the relative cover-image prefix. See the cover-image bug below. |
The two NEXT_PUBLIC_* variables are read at publish time, not during
connect. They are only relevant when main_image.path does not contain
http.
Step-by-Step Developer App Setup
There is no developer application to register.
-
Sign in to Hashnode.
-
Confirm that the target publication is on a Hashnode Pro plan. Hashnode’s current docs and changelog state that the headless GraphQL API is a Pro feature (since 13 May 2026). The official
gql-skilllistspublishPostand publication-scoped reads as Pro-gated and returnsFORBIDDENwith the messagePublication does not have an active Pro planwhen the plan is missing. Upgrade under the publication dashboard’s Billing page. -
Open Account Settings → Developer → API tokens. The direct URL is https://hashnode.com/settings/developer (it redirects through login).
-
Click Generate new token, name it (for example
Rustiz), and copy the PAT. It grants full write access to every publication the account can write to; treat it as a password. -
Optionally verify it before connecting:
curl -s https://gql-beta.hashnode.com/ \ -H "Authorization: Bearer $HASHNODE_PAT" \ -H "Content-Type: application/json" \ -d '{"query":"query { me { id username name } }"}'Hashnode accepts the token with or without the
Bearerprefix. Rustiz sends the raw token with no prefix (see gotchas).
Products and permissions
- No OAuth products or scopes. A PAT has the full permissions of the account.
- The publication role still applies: an
OWNER/EDITORcan publish; aCONTRIBUTORcan create drafts but publishing may require review. - The target publication must be Pro for
publishPostand for publication-scoped reads.
Sandbox / testing mode
Hashnode has no sandbox. The API acts on the real publication.
- Use a dedicated test publication on Pro, or accept that the post is public.
publishPostpublishes immediately; Rustiz has no draft mode. To test a token without publishing, use a manualcreateDraftmutation or amequery.- The static tag catalogue can be inspected without any API call via the
tags/tagsListprovider functions.
Production app review
There is no app review and no approval queue. Access is controlled by the
publication’s Pro subscription and by the PAT. The Pro-plan check happens at
publish time, not at connect time, so a channel can connect successfully and
then fail on the first post with a FORBIDDEN error.
Connecting a Channel (CLI & MCP)
The credential payload has exactly one key:
| Key | Type | Validation regex | Notes |
|---|---|---|---|
apiKey | password | /^.{3,}$/ | Hashnode Personal Access Token |
CLI
{ "apiKey": "hashnode_pat_xxxxxxxxxxxxxxxx" }
rustiz channels connect hashnode --fields hashnode-key.json
printf '%s' '{"apiKey":"hashnode_pat_xxxxxxxxxxxxxxxx"}' \
| rustiz channels connect hashnode --fields -
Immediate result:
{ "connected": true, "pending": false, "name": "Your Hashnode name" }
MCP
{
"provider": "hashnode",
"customFields": {
"apiKey": "hashnode_pat_xxxxxxxxxxxxxxxx"
}
}
No follow-up step. Call integrationList for the channel id. Use
--fields / customFields; --external-url / externalUrl is not the
credential path for this provider.
Provider functions (triggerTool)
| Function | Parameters | Returns | Notes |
|---|---|---|---|
tags | none | [{"value": "<objectID>", "label": "<name>"}] | Serves the static 1000-entry catalogue in hashnode/tags.rs. No API call. |
tagsList | none | [{"name": "...", "logo": "...", "slug": "...", "objectID": "..."}] | Raw catalogue entries. logo is omitted when the catalogue has no logo. |
publications | none | [{"id": "...", "name": "<publication title>"}] | GraphQL me { publications(first: 50) { edges { node { id title } } } }. Only the first 50 publications are listed. |
MCP examples:
{ "integrationId": "int_...", "methodName": "tags" }
{ "integrationId": "int_...", "methodName": "publications" }
All three functions have an empty input_schema / dataSchema.
Post Settings & Media Validation
settings_schema()
CLI note: the simple
rustiz posts create --channel <id> --content "..."form cannot pass provider settings. Userustiz posts create --json post.jsonwith aCreatePostDto(type,date,posts[].settings, …) when any of these fields are required.
| Field | Type | Required | Validation / behavior |
|---|---|---|---|
title | string | Yes | minLength: 6; checked as at least 6 Unicode scalar values. |
subtitle | string | No | minLength: 2 when present. Sent as the GraphQL subtitle input. |
main_image | object | No | Cover image. Properties: id (string), path (string), alt (string), thumbnail (string URL). id and path are required by the schema. |
canonical | string | No | Sent as originalArticleURL. If it contains (post:, URL validation is skipped. |
publication | string | Yes | Publication id. Required by schema and validator. Get it from the publications tool. |
tags | array | Yes | minItems: 1; each item needs value (string, the tag objectID) and label (string, display name). Hashnode itself caps tags at 15 per post; Rustiz does not enforce that cap. |
Example object form:
{
"title": "GraphQL pagination patterns",
"subtitle": "Cursors without tears",
"main_image": { "id": "m1", "path": "https://cdn.example/cover.png" },
"canonical": "https://blog.example/graphql-pagination",
"publication": "64f0c...",
"tags": [
{ "value": "56744721958ef13879b94cad", "label": "JavaScript" }
]
}
MCP settings array:
{
"settings": [
{ "key": "title", "value": "GraphQL pagination patterns" },
{ "key": "publication", "value": "64f0c..." },
{ "key": "tags", "value": [
{ "value": "56744721958ef13879b94cad", "label": "JavaScript" }
] }
]
}
The engine adds "__type": "hashnode" automatically if missing.
validate_settings() rules
titlemust exist, be a string and have at least 6 characters.subtitle, if present and notnull, must be a string of at least 2 characters.publicationmust exist and be a string.canonical, if present and a string, must be a valid URL unless it contains(post:.tagsmust exist and be an array with at least one item. Each item must be an object with a stringvalueand a stringlabel. Unlike the editor’s tag picker, the validator does not restrict the tag to the static catalogue.main_image, if present and notnull:- must be an object;
idmust be a string;pathmust be a string;- if
RESTRICT_UPLOAD_DOMAINSis set,pathmust contain it; - the path (query string stripped) must end in
.png,.jpg,.jpeg,.gif,.webpor.mp4(case-insensitive). altandthumbnailare in the schema but are not validated here.
Media requirements
- Only
main_image.pathis used ascoverImage; the postmedia/attachmentsarray is ignored. - The cover must be a public URL (or a path the prefix logic can turn into one).
- Only the path is sent.
altandthumbnailare ignored at publish time. - Hashnode’s own limits: image uploads must be
image/*(no SVG) and at most 8 MB; request body at most 100 KB. Rustiz does not pre-check these.
check_validity() is the trait default (Ok(None)). The only media rule in
this provider is the main_image extension/domain check in
validate_settings(); the post media array is not inspected.
Plugs
No @Plug or @PostPlug methods. run_plug() and run_post_plug() use the
trait defaults and return ProviderError::Unsupported.
Implementation Notes & Gotchas
- Raw
Authorizationtoken, noBearer.graphql()sends.header("Authorization", access_token). Hashnode accepts the raw PAT in practice (the upstream provider does the same), but Hashnode’s official agent skill usesAuthorization: Bearer. If a future API change rejects raw tokens, this is the first place to look. - GraphQL errors arrive with HTTP 200.
post()checksresponse["errors"]andresponse["data"]["publishPost"]["post"]explicitly. The first error message becomes the user-facing error; if there is no error message but the payload is missing, the fallback isHashnode could not publish the post. The error details are stored as a JSON array of GraphQL errors. - Pro gate is a
bad_body, not a retry. A missing Pro plan returns a normal GraphQL error, which Rustiz surfaces as an actionable publish failure. Retrying will not help; upgrade the publication. - Relative cover images are double-prefixed.
post()builds:prefix = "{NEXT_PUBLIC_BACKEND_URL}/{NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY}"and then concatenatespathwith no separator. WithNEXT_PUBLIC_BACKEND_URL=https://back.exampleandNEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=uploads, the pathuploads/cover.pngbecomeshttps://back.example/uploadsuploads/cover.png(the test asserts exactly this). Use an absolutemain_image.pathstarting withhttpto avoid the bug, or set the static directory to an empty string. - Unknown tag objectIDs become
slug: null. The static catalogue mapsvalue(objectID) toslug. If the id is not intags.rs, the mutation containstags: [{slug: null}], which Hashnode rejects. Always use ids returned by thetagsfunction, not arbitrary tag names. - The tag catalogue is frozen at 1000 entries. New Hashnode tags will not
be selectable until
hashnode/tags.rsis regenerated.tagsListis the raw catalogue;tagsis the editor-shaped{value,label}view. - Publications are capped at 50. The query uses
first: 50; accounts with more publications cannot select the rest through the tool. publicationis mandatory. Unlike the upstream Postiz editor, Rustiz will not publish to the user’s personal blog implicitly. A publication id must be chosen.- The title limit is 6, not 2. This is stricter than Dev.to/Medium/ WordPress and comes from the Hashnode DTO.
- No refresh.
authenticatestores a 100-yearexpires_inandrefresh_token: Some("");refresh_token()returns an empty access token. Revoked PATs require reconnecting the channel. - No
x-hashnode-clientheader. Hashnode’s official skill sendsx-hashnode-client: gql-skillfor telemetry. It is not required for authentication; Rustiz omits it.
References
- Hashnode GraphQL endpoint: https://gql-beta.hashnode.com
- Hashnode developer settings: https://hashnode.com/settings/developer
- Official Hashnode GraphQL agent skill (endpoint, auth, Pro gate, limits): https://github.com/Hashnode/gql-skill
- Postiz provider settings: https://docs.postiz.com/public-api/providers/hashnode
- Postiz posting rules: https://docs.postiz.com/general/platforms/overview