Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

PropertyValue
Identifierhashnode
Display nameHashnode
Connection modecustomFields (Personal Access Token)
Editormarkdown
Channel kindsocial (article content)
Comments / threadsNo. supports_comments = false; only the first PostDetails is published
Pending / async publishingNo. supports_pending = false; success is PostStatus::Completed
AnalyticsNo. supports_analytics = false
Post analyticsNo. supports_post_analytics = false
MentionsNo. supports_mentions = false
is_between_stepsfalse
convert_to_jpegfalse
max_concurrent_job3
Character limit (max_length)10000 characters for contentMarkdown
APIGraphQL, 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.

VariableRequiredDefaultPurpose
HASHNODE_BASE_URLNohttps://gql-beta.hashnode.comOverrides 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_DOMAINSNounsetIf set, main_image.path must contain this substring.
NEXT_PUBLIC_BACKEND_URLNo""Prefix used for a relative main_image.path. See the cover-image bug below.
NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORYNo""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.

  1. Sign in to Hashnode.

  2. 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-skill lists publishPost and publication-scoped reads as Pro-gated and returns FORBIDDEN with the message Publication does not have an active Pro plan when the plan is missing. Upgrade under the publication dashboard’s Billing page.

  3. Open Account Settings → Developer → API tokens. The direct URL is https://hashnode.com/settings/developer (it redirects through login).

  4. 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.

  5. 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 Bearer prefix. 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 / EDITOR can publish; a CONTRIBUTOR can create drafts but publishing may require review.
  • The target publication must be Pro for publishPost and 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.
  • publishPost publishes immediately; Rustiz has no draft mode. To test a token without publishing, use a manual createDraft mutation or a me query.
  • The static tag catalogue can be inspected without any API call via the tags / tagsList provider 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:

KeyTypeValidation regexNotes
apiKeypassword/^.{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)

FunctionParametersReturnsNotes
tagsnone[{"value": "<objectID>", "label": "<name>"}]Serves the static 1000-entry catalogue in hashnode/tags.rs. No API call.
tagsListnone[{"name": "...", "logo": "...", "slug": "...", "objectID": "..."}]Raw catalogue entries. logo is omitted when the catalogue has no logo.
publicationsnone[{"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. Use rustiz posts create --json post.json with a CreatePostDto (type, date, posts[].settings, …) when any of these fields are required.

FieldTypeRequiredValidation / behavior
titlestringYesminLength: 6; checked as at least 6 Unicode scalar values.
subtitlestringNominLength: 2 when present. Sent as the GraphQL subtitle input.
main_imageobjectNoCover image. Properties: id (string), path (string), alt (string), thumbnail (string URL). id and path are required by the schema.
canonicalstringNoSent as originalArticleURL. If it contains (post:, URL validation is skipped.
publicationstringYesPublication id. Required by schema and validator. Get it from the publications tool.
tagsarrayYesminItems: 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

  1. title must exist, be a string and have at least 6 characters.
  2. subtitle, if present and not null, must be a string of at least 2 characters.
  3. publication must exist and be a string.
  4. canonical, if present and a string, must be a valid URL unless it contains (post:.
  5. tags must exist and be an array with at least one item. Each item must be an object with a string value and a string label. Unlike the editor’s tag picker, the validator does not restrict the tag to the static catalogue.
  6. main_image, if present and not null:
    • must be an object;
    • id must be a string;
    • path must be a string;
    • if RESTRICT_UPLOAD_DOMAINS is set, path must contain it;
    • the path (query string stripped) must end in .png, .jpg, .jpeg, .gif, .webp or .mp4 (case-insensitive).
    • alt and thumbnail are in the schema but are not validated here.

Media requirements

  • Only main_image.path is used as coverImage; the post media / attachments array is ignored.
  • The cover must be a public URL (or a path the prefix logic can turn into one).
  • Only the path is sent. alt and thumbnail are 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 Authorization token, no Bearer. 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 uses Authorization: Bearer. If a future API change rejects raw tokens, this is the first place to look.
  • GraphQL errors arrive with HTTP 200. post() checks response["errors"] and response["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 is Hashnode 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 concatenates path with no separator. With NEXT_PUBLIC_BACKEND_URL=https://back.example and NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=uploads, the path uploads/cover.png becomes https://back.example/uploadsuploads/cover.png (the test asserts exactly this). Use an absolute main_image.path starting with http to avoid the bug, or set the static directory to an empty string.
  • Unknown tag objectIDs become slug: null. The static catalogue maps value (objectID) to slug. If the id is not in tags.rs, the mutation contains tags: [{slug: null}], which Hashnode rejects. Always use ids returned by the tags function, not arbitrary tag names.
  • The tag catalogue is frozen at 1000 entries. New Hashnode tags will not be selectable until hashnode/tags.rs is regenerated. tagsList is the raw catalogue; tags is 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.
  • publication is 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. authenticate stores a 100-year expires_in and refresh_token: Some(""); refresh_token() returns an empty access token. Revoked PATs require reconnecting the channel.
  • No x-hashnode-client header. Hashnode’s official skill sends x-hashnode-client: gql-skill for telemetry. It is not required for authentication; Rustiz omits it.

References