@cli-blog/node
v0.1.3
Published
Official zero-dependency Node.js SDK for publishing and delivering content with the Cli Blog API.
Maintainers
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/nodeWith Bun:
bun add @cli-blog/nodeWith pnpm:
pnpm add @cli-blog/nodeCreate 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"); // trueFramework 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.
