hashnode-auto-publish
v0.1.0
Published
`hashnode-auto` is a small Node.js CLI for publishing or updating one Hashnode post from a JSON file.
Readme
hashnode-auto-publish
hashnode-auto is a small Node.js CLI for publishing or updating one Hashnode post from a JSON file.
It is built for the config shape you described:
- Markdown comes from
blog_path - Title, slug, SEO title, and SEO description come from the JSON
- Cover image and OG/meta image come from
thumbnail_path - Tags come from
primary_keywordandsecondary_keyword - If the slug already exists on the publication, the CLI updates the post instead of creating a duplicate
What It Does
Given one config file, the CLI will:
- Read the JSON config
- Resolve
blog_pathandthumbnail_path - Read the Markdown file
- Resolve the target publication
- Upload a local thumbnail to Hashnode's image upload flow, or reuse a remote image URL
- Check whether a post with the same slug already exists
- Call
publishPostfor a new post orupdatePostfor an existing post - Print a human-readable summary or JSON output
Requirements
- Node.js
22+ - A Hashnode personal access token
- Either:
- a Hashnode publication ID, or
- a Hashnode publication host like
yourname.hashnode.dev
Install
Install dependencies:
npm installIf you want the command available as hashnode-auto in your shell:
npm linkIf hashnode-auto is not found after linking, run it from a login shell or make sure your npm global bin directory is on PATH.
Command Name
After npm link, the CLI command is:
hashnode-autoYou can also run it directly without linking:
node ./bin/hashnode-auto.jsUsage
Basic usage:
hashnode-auto ./post-config.jsonSupported command forms:
hashnode-auto <config.json>
hashnode-auto publish <config.json>
hashnode-auto --json <config.json>
hashnode-auto --dry-run <config.json>
hashnode-auto --helpIf you run the CLI without a config argument, it will prompt:
Config path:CLI Options
--help
Prints the help text and exits.
Example:
hashnode-auto --help--json
Prints machine-readable JSON instead of plain text. This is useful when another script is calling the CLI.
Example:
hashnode-auto --json ./post-config.json--dry-run
Validates the config and builds the publish payload without making live Hashnode API changes.
Important behavior:
- It still reads your local Markdown file
- It still validates local file paths
- It does not publish
- It does not update an existing post
- It does not upload the thumbnail
- For local images, the preview value is returned as
local-file:/absolute/path/to/image
Example:
hashnode-auto --dry-run --json ./post-config.jsonEnvironment Variables
The CLI loads .env automatically through dotenv, and also reads environment variables already set in your shell.
Required Authentication
Use one of these:
HASHNODE_TOKEN=your_hashnode_pator:
HASHNODE_PAT=your_hashnode_patResolution order:
HASHNODE_TOKENHASHNODE_PAT
Publication Target
You must identify the publication using either an ID or a host.
Preferred explicit publication ID:
HASHNODE_PUBLICATION_ID=your_publication_idOr resolve by host:
HASHNODE_PUBLICATION_HOST=yourpublication.hashnode.devor:
HASHNODE_HOST_POINT=yourpublication.hashnode.devResolution order:
HASHNODE_PUBLICATION_IDHASHNODE_PUBLICATION_HOSTHASHNODE_HOST_POINT
If a publication ID is present, host lookup is skipped.
Optional API Endpoint
Normally you should not change this:
HASHNODE_API_ENDPOINT=https://gql.hashnode.comThis exists mainly for testing or mocking.
Recommended .env
Example with publication ID:
HASHNODE_TOKEN=your_hashnode_pat
HASHNODE_PUBLICATION_ID=your_publication_id
HASHNODE_API_ENDPOINT=https://gql.hashnode.comExample with host lookup:
HASHNODE_PAT=your_hashnode_pat
HASHNODE_HOST_POINT=dishantsharma.hashnode.dev
HASHNODE_API_ENDPOINT=https://gql.hashnode.comConfig File Format
The CLI expects one JSON file with this structure:
{
"blog_path": "/Users/dishantsharma/blogs/forgecode-coding-harness.md",
"title": "ForgeCode Honest Review: Speed Tests, Real Trade-offs",
"seo_title": "ForgeCode vs Claude Code: Honest Speed and Benchmark Test",
"seo_description": "I ran ForgeCode against Claude Code on the same tasks. The speed is real but the benchmark claims need context. Here's an honest look at both.",
"seo_slug": "forgecode-honest-review",
"primary_keyword": "ForgeCode",
"secondary_keyword": "Claude Code",
"social_tips": [
"Post the benchmark manipulation finding",
"Lead with the speed numbers first"
],
"thumbnail_path": "/Users/dishantsharma/blog-images/forgecode-cover.webp"
}Required Fields
blog_pathtitleseo_titleseo_descriptionseo_slugthumbnail_path
Optional Fields
primary_keywordsecondary_keywordsocial_tips
Config Field Behavior
blog_path
Path to the Markdown file to publish.
- Can be absolute
- Can be relative
- Relative paths resolve from the config file's directory, not from the shell working directory
title
Post title sent to Hashnode.
seo_title
Mapped to Hashnode meta tags title.
seo_description
Mapped to Hashnode meta tags description.
seo_slug
Used as the post slug.
This is also the field used for upsert behavior:
- If the slug does not exist, the CLI publishes a new post
- If the slug already exists, the CLI updates that post
thumbnail_path
Source for the cover image and the OG/meta image.
It supports:
- a local file path like
./cover.webp - an absolute local path
- a remote HTTP or HTTPS URL
Behavior:
- If local, the CLI uploads the file first
- If already an HTTP or HTTPS URL, the CLI uses it directly
primary_keyword and secondary_keyword
These are converted into Hashnode tag inputs.
The CLI generates a slug from each keyword:
"ForgeCode"becomesforgecode"Claude Code"becomesclaude-code
Duplicate keywords are de-duplicated.
social_tips
This field is not sent to Hashnode.
It is only printed back in CLI output so you can reuse those ideas when sharing the post.
Path Resolution Rules
This part matters if you keep config files and content in different folders.
Example:
/work/publish/configs/post.json
/work/publish/posts/article.md
/work/publish/images/cover.webpIf post.json contains:
{
"blog_path": "../posts/article.md",
"thumbnail_path": "../images/cover.webp",
"title": "Example",
"seo_title": "Example",
"seo_description": "Example",
"seo_slug": "example"
}Then those relative paths are resolved from /work/publish/configs/, because that is where the config file lives.
Plain Text Output
When the CLI succeeds without --json, it prints a short summary like:
Action: published
Config: /absolute/path/to/post-config.json
Title: ForgeCode Honest Review: Speed Tests, Real Trade-offs
Slug: forgecode-honest-review
URL: https://yourpublication.hashnode.dev/forgecode-honest-review
Cover image: https://cdn.hashnode.com/...
Social tips:
1. Post the benchmark manipulation finding
2. Lead with the speed numbers firstPossible Action values:
publishedupdateddry-run
JSON Output
When --json is used and the command succeeds, the output looks like:
{
"ok": true,
"configPath": "/absolute/path/to/post-config.json",
"action": "published",
"coverImageUrl": "https://cdn.hashnode.com/...",
"input": {
"publicationId": "698767df7e645ee90a45b28a"
},
"post": {
"id": "post_id",
"title": "Post title",
"slug": "post-slug",
"url": "https://yourpublication.hashnode.dev/post-slug"
},
"socialTips": [
"Tip one"
]
}On failure with --json, the CLI writes:
{
"ok": false,
"error": "Error message"
}Publish vs Update Behavior
The CLI always works by slug.
Flow:
- Resolve the publication
- Look up
publication.post(slug: seo_slug) - If a post exists, call
updatePost - If no post exists, call
publishPost
This means you can safely rerun the same config file to update an existing post instead of generating a second copy with the same content.
Images
Local Image Flow
If thumbnail_path points to a local file:
- The CLI determines the MIME type from the file extension
- It asks Hashnode for a presigned image upload target
- It uploads the image
- It derives the final image URL
- It uses that URL for:
- cover image
- OG image
- meta image
Supported local extensions:
.png.jpg.jpeg.webp.gif.avif
Remote Image Flow
If thumbnail_path already starts with http:// or https://, the CLI skips upload and uses the URL directly.
Hashnode Fields Mapped by the CLI
The CLI currently sends:
titlecontentMarkdownslugcoverImageOptions.coverImageURLmetaTags.titlemetaTags.descriptionmetaTags.imagesettings.slugOverriddentags
It does not currently set:
- subtitle
- series
- publish date override
- newsletter settings
- banner image
- co-authors
Common Examples
Publish using host lookup
export HASHNODE_PAT=your_pat
export HASHNODE_HOST_POINT=dishantsharma.hashnode.dev
hashnode-auto ./configs/forgecode.jsonPublish using publication ID
export HASHNODE_TOKEN=your_pat
export HASHNODE_PUBLICATION_ID=698767df7e645ee90a45b28a
hashnode-auto ./configs/forgecode.jsonDry-run before publishing
hashnode-auto --dry-run --json ./configs/forgecode.jsonRun without linking
node ./bin/hashnode-auto.js --json ./configs/forgecode.jsonError Cases
Typical failures include:
- missing
HASHNODE_TOKENandHASHNODE_PAT - missing publication target env vars
- invalid config JSON
- missing required config fields
- markdown file not found
- thumbnail file not found
- invalid or expired Hashnode PAT
- publication host not found
- Hashnode API validation errors
Examples of actual messages you may see:
Error: Missing environment variable: one of HASHNODE_TOKEN, HASHNODE_PATError: Missing publication target: set HASHNODE_PUBLICATION_ID or one of HASHNODE_PUBLICATION_HOST, HASHNODE_HOST_POINTError: Markdown file not found: /path/to/post.mdSafety Notes
- The CLI can publish to a real production publication
- Test with
--dry-runfirst if you are unsure - If you pasted a PAT into chat or logs, rotate it afterward
- If you use the same slug repeatedly, the CLI will update the existing post
Development
Run tests:
npm testCheck help:
hashnode-auto --helpFiles
- CLI entry point: bin/hashnode-auto.js
- CLI behavior: src/cli.js
- Config loading: src/config.js
- Hashnode API client: src/hashnode-api.js
- Publish/update flow: src/publish.js
