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

@cli-blog/node

v0.1.3

Published

Official zero-dependency Node.js SDK for publishing and delivering content with the Cli Blog API.

Readme

@cli-blog/node

Official Node.js SDK for the Cli Blog API.

Homepage · Documentation · SDK Docs · API Reference · Agent Skill · GitHub

What Is This?

@cli-blog/node lets trusted JavaScript runtimes publish and deliver Cli Blog content through the public /v1 API. Use it from servers, build jobs, CI, CLIs, and AI agent runtimes to work with posts, authors, media, categories, tags, locales, sitemap XML, feed XML, revisions, and slug redirects.

The SDK is ESM-first, requires Node.js 20 or newer, uses native fetch, Blob, and FormData, and has no runtime dependencies.

Install

With npm:

npm install @cli-blog/node

With Bun:

bun add @cli-blog/node

With pnpm:

pnpm add @cli-blog/node

Create a client with an organization API key:

import { CliBlog } from "@cli-blog/node";

const blog = new CliBlog({
  apiKey: process.env.CLI_BLOG_API_KEY!,
});

Use public keys for published-content reads. Use private keys only from trusted servers, CI, CLIs, and agent runtimes. Never expose private keys in browser code.

Quick Example

Create and publish a San Francisco story:

const author = await blog.authors.create({
  public_name: "Maya Chen",
  bio: "Field notes from San Francisco.",
});

const category = await blog.categories.create({
  name: "San Francisco",
  locale: "en-US", // optional; omit to use your organization's default locale.
});

const tag = await blog.tags.create({
  name: "City Notes",
  locale: "en-US", // optional.
});

const draft = await blog.posts.create({
  title: "A developer's guide to San Francisco",
  body_markdown: "## Fog, hills, and neighborhoods\n\nA short guide to building and wandering in San Francisco.",
  author_profile_ids: [author.id],
  category_ids: [category.id],
  tag_ids: [tag.id],
  locale: "en-US", // optional.
  seo_title: "A developer's guide to San Francisco",
  seo_description: "A local story about parks, neighborhoods, and builder life in San Francisco.",
});

const published = await blog.posts.publish(draft.id, {
  expected_version: draft.version,
});

Expected result shape:

{
  id: "post_...",
  object: "post",
  locale: "en-US",
  status: "published",
  title: "A developer's guide to San Francisco",
  slug: "developers-guide-to-san-francisco",
  version: 2,
  published_at: "2026-06-18T16:00:00.000Z",
}

Resources

All list methods return:

{
  object: "list",
  data: [],
  has_more: false,
  next_cursor: null,
}

Use limit to control page size. Use after with next_cursor to fetch the next page. Use paginate() on posts when you want the SDK to follow cursors for you. Unless a field is labeled required, it is optional.

Posts

| Method | Use it for | Common parameters | | --- | --- | --- | | blog.posts.list(params) | List posts. | Optional: status, locale, limit, after, search, sort, direction, fields, include, is_featured, author/category/tag filters. | | blog.posts.paginate(params) | Iterate through all matching posts. | Same as list. | | blog.posts.get(idOrSlug, params) | Fetch one post by ID or slug. | Optional: locale, fields, include. | | blog.posts.create(input) | Create a draft, scheduled, or published post. | Required: title. Optional: body_markdown, locale, status, author_profile_ids, category_ids, tag_ids, SEO fields. | | blog.posts.update(idOrSlug, input, params) | Update a post. | Optional: expected_version, fields to change, locale lookup. | | blog.posts.publish(idOrSlug, input, params) | Publish a post. | Optional: expected_version, published_at, locale. | | blog.posts.schedule(idOrSlug, scheduledAt, input, params) | Schedule a post. | ISO datetime and optional expected_version. | | blog.posts.delete(idOrSlug, params) | Archive/delete a post through the API. | Optional locale. |

Post filters:

const posts = await blog.posts.list({
  status: "published",
  locale: "en-US", // optional; omit to use your organization's default locale.
  limit: 20,
  search: "coffee",
  sort: "published_at",
  direction: "desc",
  fields: ["summary", "seo"],
  include: ["authors", "categories", "tags", "media"],
  category_slug: "san-francisco",
  tag_slug: ["city-notes", "parks"],
});

Expected result shape:

{
  object: "list",
  data: [
    {
      id: "post_123",
      object: "post",
      title: "A developer's guide to San Francisco",
      slug: "developers-guide-to-san-francisco",
      status: "published",
      locale: "en-US",
      seo_title: "A developer's guide to San Francisco",
      authors: [{ id: "author_123", object: "author", public_name: "Maya Chen" }],
      categories: [{ id: "term_123", object: "taxonomy_term", name: "San Francisco" }],
      tags: [{ id: "term_456", object: "taxonomy_term", name: "City Notes" }],
      media: [{ id: "media_123", object: "media_asset", url: "https://..." }],
    },
  ],
  has_more: false,
  next_cursor: null,
}

Field groups control which post fields are returned:

| Field group | Use it when you need | | --- | --- | | summary | IDs, title, slug, locale, status, excerpt, timestamps. | | content | Markdown body and content fields. | | seo | SEO, robots, Open Graph, Twitter, and schema fields. | | workflow | Editorial state such as scheduling and version fields. | | metadata | Custom metadata. |

Includes add related objects:

| Include | Adds | | --- | --- | | authors | Author profile objects. | | categories | Category term objects. | | tags | Tag term objects. | | media | Referenced media asset objects. | | translations | Linked translation summaries. |

Authors

| Method | Use it for | Common parameters | | --- | --- | --- | | blog.authors.list({ limit, after }) | List public author profiles. | limit, after. | | blog.authors.get(idOrSlug) | Fetch an author. | Author ID or slug. | | blog.authors.create(input) | Create an author. | Required: public_name. Optional: slug, bio, avatar_media_id, website_url, metadata. | | blog.authors.update(idOrSlug, input) | Update an author. | Any editable author field. | | blog.authors.delete(idOrSlug) | Delete an author. | Author ID or slug. |

const author = await blog.authors.create({
  public_name: "Maya Chen",
  bio: "Field notes from San Francisco.",
  website_url: "https://example.com/authors/maya-chen",
});

Expected result shape:

{
  id: "author_123",
  object: "author",
  public_name: "Maya Chen",
  slug: "maya-chen",
  bio: "Field notes from San Francisco.",
  avatar_media_id: null,
  avatar_url: null,
  website_url: "https://example.com/authors/maya-chen",
}

Media

| Method | Use it for | Common parameters | | --- | --- | --- | | blog.media.list({ limit, after }) | List uploaded media assets. | limit, after. | | blog.media.get(id) | Fetch one media asset. | Media ID. | | blog.media.upload(input) | Upload a file. | file, filename, alt_text, caption, metadata. | | blog.media.update(id, input) | Update media metadata. | alt_text, caption, metadata. | | blog.media.delete(id) | Delete a media asset. | Media ID. |

import { readFile } from "node:fs/promises";

const file = new Blob([await readFile("bay-walk.png")], { type: "image/png" });

const media = await blog.media.upload({
  file,
  filename: "bay-walk.png",
  alt_text: "Morning light over San Francisco Bay",
  caption: "A local image for a San Francisco story.",
});

Expected result shape:

{
  id: "media_123",
  object: "media_asset",
  url: "https://cdn.example.com/media/bay-walk.png",
  original_filename: "bay-walk.png",
  alt_text: "Morning light over San Francisco Bay",
  caption: "A local image for a San Francisco story.",
  mime_type: "image/png",
  width: 1600,
  height: 900,
}

Categories And Tags

Categories and tags use the same methods. Categories can have parent categories; tags are flat labels.

| Method | Use it for | Common parameters | | --- | --- | --- | | blog.categories.list(params) / blog.tags.list(params) | List taxonomy terms. | locale, include, limit, after. | | blog.categories.get(idOrSlug, params) / blog.tags.get(idOrSlug, params) | Fetch a term. | locale, include. | | blog.categories.create(input) / blog.tags.create(input) | Create a term. | name, slug, locale, description, SEO fields, translation_of_id. | | blog.categories.update(idOrSlug, input, params) / blog.tags.update(idOrSlug, input, params) | Update a term. | Any editable term field, optional locale. | | blog.categories.delete(idOrSlug, params) / blog.tags.delete(idOrSlug, params) | Delete a term. | Optional locale. |

const category = await blog.categories.create({
  name: "San Francisco",
  locale: "en-US", // optional.
  description: "Neighborhood guides, food notes, and local stories.",
});

const tag = await blog.tags.create({
  name: "City Notes",
  locale: "en-US", // optional.
});

Expected result shape:

{
  id: "term_123",
  object: "taxonomy_term",
  taxonomy_type: "category",
  locale: "en-US",
  name: "San Francisco",
  slug: "san-francisco",
  description: "Neighborhood guides, food notes, and local stories.",
  translations: undefined,
}

Use include: ["translations"] when you need translation summaries:

const categories = await blog.categories.list({
  locale: "es-MX", // optional; include it when reading a specific language.
  include: ["translations"],
});

Locales

const locales = await blog.locales.list();

Expected result shape:

[
  { tag: "en-US", name: "English (United States)", language: "English", region: "United States" },
  { tag: "es-MX", name: "Spanish (Mexico)", language: "Spanish", region: "Mexico" },
]

Revisions And Redirects

const revisions = await blog.posts.revisions.list("developers-guide-to-san-francisco", {
  locale: "en-US", // optional.
  limit: 10,
});

const revision = await blog.posts.revisions.get(
  "developers-guide-to-san-francisco",
  revisions.data[0]!.id,
  { locale: "en-US" }, // optional.
);

const redirect = await blog.posts.slugRedirects.get("old-san-francisco-guide", {
  locale: "en-US", // optional.
});

Expected result shape:

{
  revision: {
    id: "rev_123",
    object: "post_revision",
    parent_post_id: "post_123",
    title: "A developer's guide to San Francisco",
    version: 1,
    body_markdown: "## Fog, hills...",
  },
  redirect: {
    object: "slug_redirect",
    from_slug: "old-san-francisco-guide",
    to_slug: "developers-guide-to-san-francisco",
    status_code: 301,
  },
}

Sitemap And Feed

const sitemapXml = await blog.sitemap.get({ locale: "en-US", limit: 100 }); // locale is optional.
const feedXml = await blog.feed.get({ locale: "en-US", limit: 20 }); // locale is optional.

Expected result shape:

sitemapXml.startsWith("<?xml"); // true
feedXml.includes("<rss"); // true

Framework Examples

Next.js App Router

Use the SDK in server components, route handlers, or server actions. Do not import it into client components with a private key.

// app/blog/page.tsx
import { CliBlog } from "@cli-blog/node";

const blog = new CliBlog({ apiKey: process.env.CLI_BLOG_PUBLIC_KEY! });

export default async function BlogPage() {
  const posts = await blog.posts.list({
    status: "published",
    fields: ["summary", "seo"],
    include: ["authors"],
    locale: "en-US",
  });

  return posts.data.map((post) => <article key={post.id}>{post.title}</article>);
}

Next.js Route Handler

// app/api/blog/posts/route.ts
import { CliBlog } from "@cli-blog/node";

const blog = new CliBlog({ apiKey: process.env.CLI_BLOG_PUBLIC_KEY! });

export async function GET() {
  const posts = await blog.posts.list({ status: "published", limit: 10 });
  return Response.json(posts);
}

Astro

---
import { CliBlog } from "@cli-blog/node";

const blog = new CliBlog({ apiKey: import.meta.env.CLI_BLOG_PUBLIC_KEY });
const posts = await blog.posts.list({ status: "published", fields: ["summary"] });
---

{posts.data.map((post) => <article><h2>{post.title}</h2></article>)}

React Or Vite

React apps run in the browser, so do not put private keys there. Create a small server route with the SDK, then call that route from React.

// React component
const response = await fetch("/api/blog/posts");
const posts = await response.json();

Remix Or React Router

// app/routes/blog._index.tsx
import { CliBlog } from "@cli-blog/node";

export async function loader() {
  const blog = new CliBlog({ apiKey: process.env.CLI_BLOG_PUBLIC_KEY! });
  return blog.posts.list({ status: "published", fields: ["summary"] });
}

AI Agent Skill

If you want an AI coding agent to add Cli Blog to an application, use the Cli Blog agent skill. It includes guidance for choosing the API, SDK, or CLI, plus framework patterns for common app stacks.

Errors

The SDK throws CliBlogError for API errors and client setup failures.

import { CliBlogError } from "@cli-blog/node";

try {
  await blog.posts.create({ title: "Draft" });
} catch (error) {
  if (error instanceof CliBlogError) {
    console.error({
      code: error.code,
      field: error.field,
      message: error.message,
      requestId: error.requestId,
      status: error.status,
    });
  }
  throw error;
}

Common cases:

| Status / code | When to expect it | What to do | | --- | --- | --- | | missing_api_key | The client was created without an API key. | Pass apiKey from a secret or environment variable. | | 401 | The key is missing or invalid. | Check the key value and organization. | | 403 / forbidden | The key type or scopes do not allow the action. | Use a private key for trusted writes and the right permissions. | | 404 / not_found | The resource ID or locale-scoped slug does not exist. | Check the ID, slug, and locale. | | 409 | Optimistic concurrency failed, usually from stale expected_version. | Fetch the latest post and retry with the current version. | | 429 | Rate or plan limit reached. | Back off or upgrade the organization plan. | | 5xx | Temporary API or upstream failure. | Retry later; safe requests are retried automatically by the SDK. |

Safe read requests are retried automatically on transient statuses such as 408, 409, 425, 429, and 5xx. Mutating requests are not retried automatically.

Security

  • Never expose private API keys in browser code.
  • Prefer environment variables or secret managers for private keys.
  • Use public keys for published delivery reads.
  • Use private keys for trusted publishing and editorial workflows.
  • The SDK uses native platform APIs and does not add runtime dependencies.