npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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_keyword and secondary_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:

  1. Read the JSON config
  2. Resolve blog_path and thumbnail_path
  3. Read the Markdown file
  4. Resolve the target publication
  5. Upload a local thumbnail to Hashnode's image upload flow, or reuse a remote image URL
  6. Check whether a post with the same slug already exists
  7. Call publishPost for a new post or updatePost for an existing post
  8. 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 install

If you want the command available as hashnode-auto in your shell:

npm link

If 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-auto

You can also run it directly without linking:

node ./bin/hashnode-auto.js

Usage

Basic usage:

hashnode-auto ./post-config.json

Supported command forms:

hashnode-auto <config.json>
hashnode-auto publish <config.json>
hashnode-auto --json <config.json>
hashnode-auto --dry-run <config.json>
hashnode-auto --help

If 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.json

Environment 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_pat

or:

HASHNODE_PAT=your_hashnode_pat

Resolution order:

  1. HASHNODE_TOKEN
  2. HASHNODE_PAT

Publication Target

You must identify the publication using either an ID or a host.

Preferred explicit publication ID:

HASHNODE_PUBLICATION_ID=your_publication_id

Or resolve by host:

HASHNODE_PUBLICATION_HOST=yourpublication.hashnode.dev

or:

HASHNODE_HOST_POINT=yourpublication.hashnode.dev

Resolution order:

  1. HASHNODE_PUBLICATION_ID
  2. HASHNODE_PUBLICATION_HOST
  3. HASHNODE_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.com

This 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.com

Example with host lookup:

HASHNODE_PAT=your_hashnode_pat
HASHNODE_HOST_POINT=dishantsharma.hashnode.dev
HASHNODE_API_ENDPOINT=https://gql.hashnode.com

Config 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_path
  • title
  • seo_title
  • seo_description
  • seo_slug
  • thumbnail_path

Optional Fields

  • primary_keyword
  • secondary_keyword
  • social_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" becomes forgecode
  • "Claude Code" becomes claude-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.webp

If 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 first

Possible Action values:

  • published
  • updated
  • dry-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:

  1. Resolve the publication
  2. Look up publication.post(slug: seo_slug)
  3. If a post exists, call updatePost
  4. 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:

  1. The CLI determines the MIME type from the file extension
  2. It asks Hashnode for a presigned image upload target
  3. It uploads the image
  4. It derives the final image URL
  5. 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:

  • title
  • contentMarkdown
  • slug
  • coverImageOptions.coverImageURL
  • metaTags.title
  • metaTags.description
  • metaTags.image
  • settings.slugOverridden
  • tags

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

Publish using publication ID

export HASHNODE_TOKEN=your_pat
export HASHNODE_PUBLICATION_ID=698767df7e645ee90a45b28a
hashnode-auto ./configs/forgecode.json

Dry-run before publishing

hashnode-auto --dry-run --json ./configs/forgecode.json

Run without linking

node ./bin/hashnode-auto.js --json ./configs/forgecode.json

Error Cases

Typical failures include:

  • missing HASHNODE_TOKEN and HASHNODE_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_PAT
Error: Missing publication target: set HASHNODE_PUBLICATION_ID or one of HASHNODE_PUBLICATION_HOST, HASHNODE_HOST_POINT
Error: Markdown file not found: /path/to/post.md

Safety Notes

  • The CLI can publish to a real production publication
  • Test with --dry-run first 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 test

Check help:

hashnode-auto --help

Files