jkunz f6e670f08c feat(formats): typed formats option on browse, browseAll and read
The Content and Admin API clients take a `formats` option for posts and
pages on `browse`, `browseAll` and `read`, sent comma-separated on every
page. `TGhostFormat` covers the Admin values (html, plaintext, lexical,
mobiledoc); `TGhostContentFormat` limits the Content client to html and
plaintext, because the Content API drops the others without an error.
The option interfaces are generic over the format and exported for
annotating calls made through the untyped `Ghost.adminApi` and
`Ghost.contentApi`.

The Admin `read` query is typed; TypeScript callers of `GhostAdminAPI`
that pass other keys no longer compile, runtime behaviour is unchanged.
Two Content `read` calls in the synced instance lose a second argument
that was never read.
2026-10-22 20:55:34 +00:00
2024-07-01 16:32:22 +02:00
2024-07-01 16:32:22 +02:00
2024-07-01 16:32:22 +02:00
2025-10-10 17:02:41 +00:00
2025-10-11 06:16:45 +00:00
2025-10-10 17:02:41 +00:00
2024-07-01 16:32:22 +02:00
2024-07-01 16:32:22 +02:00

@apiclient.xyz/ghost 👻

The unofficial TypeScript-first Ghost CMS API client that actually makes sense

A modern, fully-typed API client for Ghost CMS that wraps both the Content and Admin APIs into an elegant, developer-friendly interface. Built with TypeScript, designed for humans.

✨ What Makes This Different?

Unlike the official Ghost SDK, this library gives you:

  • One unified client instead of juggling separate Content and Admin API instances
  • Class-based models with helper methods instead of raw JSON objects
  • Built-in JWT generation so you don't need to handle tokens manually
  • Pattern matching with minimatch for flexible filtering
  • Multi-instance sync for managing content across staging/production environments
  • Complete tag support including tags with zero posts (Content API limitation bypassed)
  • Universal runtime support - works in Node.js, Deno, Bun, and browsers without different packages

🚀 Why This Library?

  • 🎯 TypeScript Native - Full type safety for all Ghost API operations with comprehensive interfaces
  • 🔥 Dual API Support - Unified interface for both Content and Admin APIs, seamlessly integrated
  • ⚡ Modern Async/Await - No callback hell, just clean promises and elegant async patterns
  • 🌐 Universal Compatibility - Native fetch implementation works in Node.js, Deno, Bun, and browsers
  • 🎨 Elegant API - Intuitive methods that match your mental model, not Ghost's quirks
  • 🔍 Smart Filtering - Built-in minimatch support for flexible pattern-based queries
  • 🏷️ Complete Tag Support - Fetch ALL tags (including zero-count), filter by visibility (internal/external)
  • 🔄 Multi-Instance Sync - Synchronize content across multiple Ghost sites with built-in safety checks
  • 📅 ISO 8601 Dates - All dates are properly formatted ISO 8601 strings with timezone support
  • 🛡️ Built-in JWT Generation - Automatic JWT token handling for Admin API authentication
  • 💪 Production Ready - Battle-tested with 139+ comprehensive tests across Node.js and Deno

📖 Table of Contents

📦 Installation

npm install @apiclient.xyz/ghost

Or with pnpm:

pnpm install @apiclient.xyz/ghost

🎯 Quick Start

import { Ghost } from '@apiclient.xyz/ghost';

const ghost = new Ghost({
  baseUrl: 'https://your-ghost-site.com',
  contentApiKey: 'your_content_api_key',  // Optional: only needed for reading
  adminApiKey: 'your_admin_api_key'       // Required for write operations
});

// Read posts
const posts = await ghost.getPosts({ limit: 10 });
posts.forEach(post => console.log(post.getTitle()));

// Create a post
const newPost = await ghost.createPost({
  title: 'Hello World',
  html: '<p>My first post!</p>',
  status: 'published'
});

// Update it
await newPost.update({
  title: 'Hello World - Updated!'
});

That's it. No complicated setup, no boilerplate. Just pure Ghost API goodness. 🎉

📚 Core API

🔑 Authentication & Setup

Initialize the Ghost client with your API credentials:

const ghost = new Ghost({
  baseUrl: 'https://your-ghost-site.com',
  contentApiKey: 'your_content_api_key',  // For reading public content
  adminApiKey: 'your_admin_api_key'       // For write operations
});

📑 Pagination and Limits

Every list method follows Ghost's meta.pagination and returns all results, not only the first page: getPosts, searchPosts, getRelatedPosts, getPages, getTags (with getPublicTags and getInternalTags), getAuthors and getMembers.

Ghost 6 returns at most 100 items per request, and a site can configure a lower cap. The client asks for 100 items per page and follows next until Ghost reports no further page, so it pages correctly whatever cap the server applies. The page size is internal; you never set it.

const allPosts = await ghost.getPosts();           // every post, page after page
const latest = await ghost.getPosts({ limit: 5 }); // at most 5 posts, one request

limit is a total cap: the maximum number of items a list method returns. Without it, the method returns every result.

  • For getTags, getPages, getAuthors and getMembers, the cap applies after the minimatch filter, so getTags({ filter: 'news-*', limit: 2 }) returns the first two matching tags even when they sit on a later page.
  • getRelatedPosts(postId, limit) keeps its default of 5; larger values, including values above 100, work.
  • limit must be a positive integer. Anything else (0, -1, 2.5) throws a RangeError before any request is made.

Why a cap and not a page size: callers read limit as "at most N" (getPosts({ limit: 10 }) for the latest ten posts), and a page size on a method that returns a flat array would hand back partial results without saying so.

Ordering: posts, pages and members keep Ghost's default order (posts newest first). Tags and authors are requested with order: 'id desc' (newest first), because Ghost's default order for them is not unique, and paging over an order that is not unique can skip or repeat items.

Errors are never swallowed, and every inconsistency the client can detect rejects the call, so it never returns a list it knows to be partial:

  • If any page fails, the whole call rejects with Ghost's error (its message, name and statusCode).
  • If the paging itself is inconsistent, the call rejects with a GhostPaginationError. That covers a response without meta.pagination, a page other than the one requested, a total or pages count that changes between pages, a next that is not the following page, an item without an id, a duplicate id, and a final count that differs from total. Most of these mean the collection changed while the client was paging (for example, a post was published between two page requests). Calling the method again is safe.
import { GhostPaginationError } from '@apiclient.xyz/ghost';

try {
  const posts = await ghost.getPosts();
} catch (error) {
  if (error instanceof GhostPaginationError) {
    // the collection changed while paging; try again later
  }
  throw error;
}

What offset paging cannot see: if one item is deleted and another inserted in the same gap between two page requests, so that total stays the same and nothing is pushed onto a page already fetched, the client cannot detect it. Ghost has no cursor paging to close that gap. The id desc order for tags and authors keeps such a pair from skipping a live item, because the new item sorts first.

For direct access, every resource of the bundled API clients has browseAll(options?) next to browse(options?). browseAll takes filter, include, fields, order and max (the total cap), and pages through the same way. It adds no order of its own, so pass a unique one (for example order: 'id desc') for tags, authors and users, whose Ghost default order is not unique. If you set fields, include id: the pager checks ids for duplicates. browse returns one page and takes Ghost's own limit and page:

const tags = await ghost.adminApi.tags.browseAll({ order: 'id desc', max: 500 });
const firstPage = await ghost.contentApi.posts.browse({ limit: 15, page: 1 });

browse, browseAll and read also take formats, the list of body formats Ghost returns for posts and pages; other resources ignore it. The Admin API accepts html, plaintext, lexical and mobiledoc and returns mobiledoc and lexical by default. A passed formats replaces that default, so send ['html', 'lexical'] to get both. The Content API returns html by default and serves only html and plaintext. It also accepts lexical and mobiledoc but drops them without an error, so a Content request for only those returns posts without a body. The client sends the list on every page.

const posts = await ghost.adminApi.posts.browseAll({ formats: ['html', 'lexical'] });
const post = await ghost.adminApi.posts.read({ slug: 'welcome' }, { formats: ['html'] });

ghost.adminApi and ghost.contentApi are typed any, so calls through them are not type-checked: a Content request for ['lexical'] compiles there. To have TypeScript check the options, annotate them with the exported types (IBrowseOptions, IBrowseAllOptions, IReadOptions, IReadQueryOptions, TGhostFormat, TGhostContentFormat):

import type { IBrowseAllOptions, TGhostContentFormat } from '@apiclient.xyz/ghost';

const options: IBrowseAllOptions<TGhostContentFormat> = { formats: ['html'] };
const contentPosts = await ghost.contentApi.posts.browseAll(options);

📝 Posts

Get All Posts

const posts = await ghost.getPosts();

posts.forEach(post => {
  console.log(post.getTitle());
  console.log(post.getExcerpt());
  console.log(post.getFeatureImage());
});

Filter Posts

const techPosts = await ghost.getPosts({ 
  tag: 'technology',
  limit: 10 
});

const featuredPosts = await ghost.getPosts({ 
  featured: true,
  limit: 5 
});

const authorPosts = await ghost.getPosts({ 
  author: 'john-doe' 
});

Get Single Post

const post = await ghost.getPostById('post-id');
console.log(post.getTitle());
console.log(post.getHtml());
console.log(post.getAuthor());

Create Post

const newPost = await ghost.createPost({
  title: 'My Awesome Post',
  html: '<p>This is the content of my post.</p>',
  feature_image: 'https://example.com/image.jpg',
  tags: [{ id: 'tag-id' }],
  excerpt: 'A brief summary of the post'
});

Or create from HTML specifically:

const post = await ghost.createPostFromHtml({
  title: 'My HTML Post',
  html: '<p>Content here</p>'
});

Update Post

const post = await ghost.getPostById('post-id');
await post.update({
  ...post.toJson(),
  title: 'Updated Title',
  html: '<p>Updated content</p>'
});

Delete Post

const post = await ghost.getPostById('post-id');
await post.delete();

Search Posts

Full-text search across post titles:

const results = await ghost.searchPosts('typescript tutorial', { limit: 10 });
results.forEach(post => console.log(post.getTitle()));

Get posts with similar tags:

const post = await ghost.getPostById('post-id');
const related = await ghost.getRelatedPosts(post.getId(), 5);
related.forEach(p => console.log(`Related: ${p.getTitle()}`));

Bulk Operations

await ghost.bulkUpdatePosts(['id1', 'id2', 'id3'], {
  featured: true
});

await ghost.bulkDeletePosts(['id4', 'id5', 'id6']);

📄 Pages

Pages work similarly to posts but are for static content:

const pages = await ghost.getPages();

const aboutPage = await ghost.getPageBySlug('about');
console.log(aboutPage.getHtml());

const newPage = await ghost.createPage({
  title: 'Contact',
  html: '<p>Contact information...</p>'
});

await newPage.update({
  html: '<p>Updated content</p>'
});

await newPage.delete();

Filter pages with minimatch patterns:

const filteredPages = await ghost.getPages({ 
  filter: 'about*',
  limit: 10 
});

🏷️ Tags

Get All Tags

// Get ALL tags (including those with zero posts)
const tags = await ghost.getTags();
tags.forEach(tag => console.log(`${tag.name} (${tag.slug})`));

Note: Uses Admin API to fetch ALL tags, including tags with zero posts. Previous versions using Content API would omit tags with no associated content.

Filter by Visibility

Ghost supports two tag types:

  • Public tags: Standard tags visible to readers
  • Internal tags: Tags prefixed with # for internal organization (not visible publicly)
// Get only public tags
const publicTags = await ghost.getPublicTags();

// Get only internal tags (e.g., #feature, #urgent)
const internalTags = await ghost.getInternalTags();

// Get all tags with explicit visibility filter
const publicTags = await ghost.getTags({ visibility: 'public' });
const internalTags = await ghost.getTags({ visibility: 'internal' });
const allTags = await ghost.getTags({ visibility: 'all' }); // default

Filter Tags with Minimatch

const techTags = await ghost.getTags({ filter: 'tech-*' });
const blogTags = await ghost.getTags({ filter: '*blog*' });

// Combine visibility and pattern filtering
const internalNews = await ghost.getTags({
  filter: 'news-*',
  visibility: 'internal'
});

Get Single Tag

const tag = await ghost.getTagBySlug('javascript');
console.log(tag.getName());
console.log(tag.getDescription());
console.log(tag.getVisibility()); // 'public' or 'internal'

// Check visibility
if (tag.isInternal()) {
  console.log('This is an internal tag');
}

Create, Update, Delete Tags

// Create a public tag
const newTag = await ghost.createTag({
  name: 'TypeScript',
  slug: 'typescript',
  description: 'All about TypeScript',
  visibility: 'public'
});

// Create an internal tag (note the # prefix)
const internalTag = await ghost.createTag({
  name: '#feature',
  slug: 'hash-feature',
  visibility: 'internal'
});

// Update tag
await newTag.update({
  description: 'Everything TypeScript related'
});

// Delete tag (now works reliably!)
await newTag.delete();

👤 Authors

Get Authors

const authors = await ghost.getAuthors();
authors.forEach(author => {
  console.log(`${author.getName()} (${author.getSlug()})`);
});

Filter Authors

const filteredAuthors = await ghost.getAuthors({ 
  filter: 'j*',
  limit: 10 
});

Get Single Author

const author = await ghost.getAuthorBySlug('john-doe');
console.log(author.getBio());
console.log(author.getProfileImage());

Update Author

await author.update({
  bio: 'Updated bio information',
  website: 'https://johndoe.com'
});

👥 Members

Manage your Ghost site members (requires Ghost membership features):

Get Members

const members = await ghost.getMembers({ limit: 100 });
members.forEach(member => {
  console.log(`${member.getName()} - ${member.getEmail()}`);
  console.log(`Status: ${member.getStatus()}`);
});

Filter Members

const gmailMembers = await ghost.getMembers({ 
  filter: '*@gmail.com' 
});

Get Single Member

const member = await ghost.getMemberByEmail('user@example.com');
console.log(member.getStatus());
console.log(member.getLabels());

Create Member

const newMember = await ghost.createMember({
  email: 'newuser@example.com',
  name: 'New User',
  note: 'VIP member'
});

Update and Delete Members

await member.update({
  name: 'Updated Name',
  note: 'Premium member'
});

await member.delete();

🪝 Webhooks

Manage webhooks for Ghost events:

const webhook = await ghost.createWebhook({
  event: 'post.published',
  target_url: 'https://example.com/webhook',
  name: 'Post Published Webhook'
});

await ghost.updateWebhook(webhook.id, {
  target_url: 'https://example.com/new-webhook'
});

await ghost.deleteWebhook(webhook.id);

Note: The Ghost Admin API only supports creating, updating, and deleting webhooks. Browsing and reading individual webhooks are not supported by the underlying SDK.

🖼️ Image Upload

Upload images to your Ghost site:

const imageUrl = await ghost.uploadImage('/path/to/image.jpg');

await ghost.createPost({
  title: 'Post with Image',
  html: '<p>Content here</p>',
  feature_image: imageUrl
});

🔄 Multi-Instance Synchronization

The SyncedInstance class enables you to synchronize content across multiple Ghost instances - perfect for staging environments, multi-region deployments, or content distribution.

Key Features:

  • 🔒 Same-Instance Protection - Automatically prevents circular syncs that would cause excessive API calls
  • 🏷️ Slug Congruence - Ensures slugs remain consistent across all synced instances
  • 🗺️ ID Mapping - Tracks source-to-target ID mappings for efficient updates
  • 📊 Detailed Reporting - Get comprehensive sync reports with success/failure counts

Setup

import { Ghost, SyncedInstance } from '@apiclient.xyz/ghost';

const sourceGhost = new Ghost({
  baseUrl: 'https://source.ghost.com',
  contentApiKey: 'source_content_key',
  adminApiKey: 'source_admin_key'
});

const targetGhost1 = new Ghost({
  baseUrl: 'https://target1.ghost.com',
  contentApiKey: 'target1_content_key',
  adminApiKey: 'target1_admin_key'
});

const targetGhost2 = new Ghost({
  baseUrl: 'https://target2.ghost.com',
  contentApiKey: 'target2_content_key',
  adminApiKey: 'target2_admin_key'
});

// This will throw an error if you accidentally try to sync an instance to itself
const synced = new SyncedInstance(sourceGhost, [targetGhost1, targetGhost2]);

Safety Note: SyncedInstance validates that the source and target instances are different. Attempting to sync an instance to itself will throw an error immediately, preventing circular syncs and rate limit issues.

Sync Content

const tagReport = await synced.syncTags();
console.log(`Synced ${tagReport.totalItems} tags`);
console.log(`Duration: ${tagReport.duration}ms`);

const postReport = await synced.syncPosts();
console.log(`Success: ${postReport.targetReports[0].successCount}`);
console.log(`Failed: ${postReport.targetReports[0].failureCount}`);

const pageReport = await synced.syncPages();

Sync Options

const report = await synced.syncPosts({
  filter: 'featured-*',
  dryRun: true,
  incremental: true
});

report.targetReports.forEach(targetReport => {
  console.log(`Target: ${targetReport.targetUrl}`);
  targetReport.results.forEach(result => {
    console.log(`  ${result.sourceSlug}: ${result.status}`);
  });
});

Sync Everything

const reports = await synced.syncAll({
  types: ['tags', 'posts', 'pages'],
  syncOptions: {
    dryRun: false
  }
});

reports.forEach(report => {
  console.log(`${report.contentType}: ${report.totalItems} items`);
});

Sync Status & History

const status = synced.getSyncStatus();
console.log(`Total mappings: ${status.totalMappings}`);
console.log(`Recent syncs: ${status.recentSyncs.length}`);

status.mappings.forEach(mapping => {
  console.log(`Source: ${mapping.sourceSlug}`);
  mapping.targetMappings.forEach(tm => {
    console.log(`  -> ${tm.targetUrl} (${tm.targetId})`);
  });
});

synced.clearSyncHistory();
synced.clearMappings();

🎨 Complete Example

Here's a comprehensive example showing various operations:

import { Ghost, SyncedInstance } from '@apiclient.xyz/ghost';

const ghost = new Ghost({
  baseUrl: 'https://your-ghost-site.com',
  contentApiKey: 'your_content_key',
  adminApiKey: 'your_admin_key'
});

async function createBlogPost() {
  // Upload a feature image
  const imageUrl = await ghost.uploadImage('./banner.jpg');

  // Create a tag for categorization
  const tag = await ghost.createTag({
    name: 'Tutorial',
    slug: 'tutorial',
    description: 'Step-by-step guides',
    visibility: 'public'
  });

  // Create a comprehensive blog post
  const post = await ghost.createPost({
    title: 'Getting Started with Ghost CMS',
    slug: 'getting-started-ghost-cms',
    html: '<h1>Welcome</h1><p>This is an introduction to Ghost CMS...</p>',
    feature_image: imageUrl,
    tags: [{ id: tag.getId() }],
    featured: true,
    status: 'published',
    meta_title: 'Getting Started with Ghost CMS | Tutorial',
    meta_description: 'Learn how to get started with Ghost CMS in this comprehensive guide',
    custom_excerpt: 'A beginner-friendly guide to Ghost CMS'
  });

  console.log(`✅ Created post: ${post.getTitle()}`);
  console.log(`📅 Published at: ${post.postData.published_at}`);

  // Find related content
  const related = await ghost.getRelatedPosts(post.getId(), 5);
  console.log(`🔗 Found ${related.length} related posts`);

  // Search functionality
  const searchResults = await ghost.searchPosts('getting started', { limit: 10 });
  console.log(`🔍 Search found ${searchResults.length} posts`);

  // Get all public tags
  const publicTags = await ghost.getPublicTags();
  console.log(`🏷️  Public tags: ${publicTags.length}`);

  return post;
}

async function syncToStaging() {
  // Sync content to staging environment
  const production = new Ghost({
    baseUrl: 'https://production.ghost.com',
    adminApiKey: process.env.PROD_ADMIN_KEY,
    contentApiKey: process.env.PROD_CONTENT_KEY
  });

  const staging = new Ghost({
    baseUrl: 'https://staging.ghost.com',
    adminApiKey: process.env.STAGING_ADMIN_KEY,
    contentApiKey: process.env.STAGING_CONTENT_KEY
  });

  const synced = new SyncedInstance(production, [staging]);

  // Sync everything
  const reports = await synced.syncAll({
    types: ['tags', 'posts', 'pages']
  });

  reports.forEach(report => {
    console.log(`✅ Synced ${report.totalItems} ${report.contentType} in ${report.duration}ms`);
  });
}

// Run the examples
createBlogPost().catch(console.error);
// syncToStaging().catch(console.error);

⚡ Performance & Best Practices

Rate Limiting

Each page of a list is one API request, and Ghost may rate-limit busy clients. Keep these tips in mind:

// ✅ Good: Batch operations
await ghost.bulkUpdatePosts(['id1', 'id2', 'id3'], { featured: true });

// ❌ Bad: Individual requests in a loop
for (const id of postIds) {
  await ghost.getPostById(id).then(p => p.update({ featured: true }));
}

// ✅ Good: Ask only for what you need; `limit` caps the result, so fewer pages are fetched
const posts = await ghost.getPosts({ limit: 15 });

// ✅ Good: Filter on the server side
const featuredPosts = await ghost.getPosts({ featured: true, limit: 10 });

Multi-Instance Sync Safety

The library automatically prevents common pitfalls:

// ✅ This works - different instances
const synced = new SyncedInstance(sourceGhost, [targetGhost]);

// ❌ This throws an error - prevents circular sync!
const synced = new SyncedInstance(ghost, [ghost]); // Error: Cannot sync to same instance

Content API vs Admin API

  • Content API: Read-only, public content, no authentication required (with Content API key)
  • Admin API: Full read/write access, requires Admin API key
  • Tags: This library uses Admin API for tags to fetch ALL tags (Content API only returns tags with posts)

Dry Run Mode

Test your sync operations without making changes:

const report = await synced.syncAll({
  types: ['posts', 'pages', 'tags'],
  syncOptions: {
    dryRun: true  // Preview changes without applying them
  }
});

console.log(`Would sync ${report[0].totalItems} items`);

🔒 Error Handling

All methods throw errors that you can catch and handle:

try {
  const post = await ghost.getPostById('invalid-id');
} catch (error) {
  console.error('Failed to fetch post:', error);
}

try {
  await post.update({ title: 'New Title' });
} catch (error) {
  console.error('Failed to update post:', error);
}

📖 API Reference

Ghost Class

Method Description Returns
getPosts(options?) Get all posts (every page) with optional filtering; limit caps the count Promise<Post[]>
getPostById(id) Get a single post by ID Promise<Post>
createPost(data) Create a new post Promise<Post>
createPostFromHtml(data) Create post from HTML Promise<Post>
searchPosts(query, options?) Search posts by title; returns every match unless limit caps it Promise<Post[]>
getRelatedPosts(postId, limit?) Get related posts, at most limit (default 5) Promise<Post[]>
bulkUpdatePosts(ids, updates) Update multiple posts Promise<Post[]>
bulkDeletePosts(ids) Delete multiple posts Promise<void>
getPages(options?) Get all pages (every page of results); limit caps the count after filter Promise<Page[]>
getPageById(id) Get page by ID Promise<Page>
getPageBySlug(slug) Get page by slug Promise<Page>
createPage(data) Create a new page Promise<Page>
getTags(options?) Get all tags (including zero-count, every page); limit caps the count after filter Promise<ITag[]>
getPublicTags(options?) Get only public tags Promise<ITag[]>
getInternalTags(options?) Get only internal tags Promise<ITag[]>
getTagById(id) Get tag by ID Promise<Tag>
getTagBySlug(slug) Get tag by slug Promise<Tag>
createTag(data) Create a new tag Promise<Tag>
getAuthors(options?) Get all authors (every page); limit caps the count after filter Promise<Author[]>
getAuthorById(id) Get author by ID Promise<Author>
getAuthorBySlug(slug) Get author by slug Promise<Author>
getMembers(options?) Get all members (every page); limit caps the count after filter Promise<Member[]>
getMemberById(id) Get member by ID Promise<Member>
getMemberByEmail(email) Get member by email Promise<Member>
createMember(data) Create a new member Promise<Member>
createWebhook(data) Create a webhook Promise<any>
updateWebhook(id, data) Update a webhook Promise<any>
deleteWebhook(id) Delete a webhook Promise<void>
uploadImage(filePath) Upload an image Promise<string>

API Clients

Method Description Returns
contentApi.<posts|pages|tags|authors>.browseAll(options?) Every page of a Content API resource; max caps the count Promise<T[]>
adminApi.<resource>.browseAll(options?) Every page of an Admin API resource; max caps the count Promise<T[]>
contentApi.<resource>.browse(options?) / adminApi.<resource>.browse(options?) One page; Ghost's limit and page Promise<T | T[]>

GhostPaginationError is exported from the package entry point; see Pagination and Limits.

Post Class

Method Description Returns
getId() Get post ID string
getTitle() Get post title string
getHtml() Get post HTML content string
getExcerpt() Get post excerpt string
getFeatureImage() Get feature image URL string | undefined
getAuthor() Get primary author IAuthor
toJson() Get raw post data IPost
update(data) Update the post Promise<Post>
delete() Delete the post Promise<void>

Page Class

Method Description Returns
getId() Get page ID string
getTitle() Get page title string
getHtml() Get page HTML content string
getSlug() Get page slug string
getFeatureImage() Get feature image URL string | undefined
getAuthor() Get primary author IAuthor
toJson() Get raw page data IPage
update(data) Update the page Promise<Page>
delete() Delete the page Promise<void>

Tag Class

Method Description Returns
getId() Get tag ID string
getName() Get tag name string
getSlug() Get tag slug string
getDescription() Get tag description string | undefined
getVisibility() Get tag visibility string
isInternal() Check if tag is internal boolean
isPublic() Check if tag is public boolean
toJson() Get raw tag data ITag
update(data) Update the tag Promise<Tag>
delete() Delete the tag Promise<void>

Author Class

Method Description Returns
getId() Get author ID string
getName() Get author name string
getSlug() Get author slug string
getProfileImage() Get profile image URL string | undefined
getBio() Get author bio string | undefined
toJson() Get raw author data IAuthor
update(data) Update the author Promise<Author>

Member Class

Method Description Returns
getId() Get member ID string
getEmail() Get member email string
getName() Get member name string | undefined
getStatus() Get member status string | undefined
getLabels() Get member labels Array | undefined
toJson() Get raw member data IMember
update(data) Update the member Promise<Member>
delete() Delete the member Promise<void>

SyncedInstance Class

Method Description Returns
syncPosts(options?) Sync posts to targets Promise<ISyncReport>
syncPages(options?) Sync pages to targets Promise<ISyncReport>
syncTags(options?) Sync tags to targets Promise<ISyncReport>
syncAll(options?) Sync all content types Promise<ISyncReport[]>
getSyncStatus() Get sync status & mappings Object
clearSyncHistory() Clear sync history void
clearMappings() Clear ID mappings void

🧪 Testing

pnpm test

test/test.pagination.node.ts runs offline against a stub server started inside the test. The other tests need a running Ghost instance and API keys.

📝 TypeScript Support

This library is written in TypeScript and provides full type definitions out of the box. No @types/* package needed.

import type { IPost, ITag, IAuthor, IMember, IPage } from '@apiclient.xyz/ghost';

Date Handling

All date fields (created_at, updated_at, published_at) are returned as ISO 8601 formatted strings with timezone information:

const post = await ghost.getPostById('post-id');

// Date strings are in ISO 8601 format: "2025-10-10T13:54:44.000-04:00"
console.log(post.postData.created_at);    // string
console.log(post.postData.updated_at);    // string
console.log(post.postData.published_at);  // string

// Parse them to Date objects if needed
const publishedDate = new Date(post.postData.published_at);
console.log(publishedDate.toISOString());

Note: Ghost automatically manages updated_at timestamps. When you update metadata fields (title, status, tags, etc.), Ghost updates this timestamp. HTML-only updates may not always change updated_at.

🐛 Issues & Feedback

Found a bug or have a feature request?

Repository: https://code.foss.global/apiclient.xyz/ghost

This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository. Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.

Company Information

Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany

For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
an unofficial ghost api package
Readme
438 KiB
Languages
TypeScript 100%