
Product
Reachability for Ruby Now in Beta
Reachability analysis for Ruby is now in beta, helping teams identify which vulnerabilities are truly exploitable in their applications.
@apiclient.xyz/ghost
Advanced tools
An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.
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.
Unlike the official Ghost SDK, this library gives you:
npm install @apiclient.xyz/ghost
Or with pnpm:
pnpm install @apiclient.xyz/ghost
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. π
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
});
const posts = await ghost.getPosts();
posts.forEach(post => {
console.log(post.getTitle());
console.log(post.getExcerpt());
console.log(post.getFeatureImage());
});
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'
});
const post = await ghost.getPostById('post-id');
console.log(post.getTitle());
console.log(post.getHtml());
console.log(post.getAuthor());
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>'
});
const post = await ghost.getPostById('post-id');
await post.update({
...post.toJson(),
title: 'Updated Title',
html: '<p>Updated content</p>'
});
const post = await ghost.getPostById('post-id');
await post.delete();
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()}`));
await ghost.bulkUpdatePosts(['id1', 'id2', 'id3'], {
featured: true
});
await ghost.bulkDeletePosts(['id4', 'id5', 'id6']);
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
});
// 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.
Ghost supports two tag types:
# 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
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'
});
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 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();
const authors = await ghost.getAuthors();
authors.forEach(author => {
console.log(`${author.getName()} (${author.getSlug()})`);
});
const filteredAuthors = await ghost.getAuthors({
filter: 'j*',
limit: 10
});
const author = await ghost.getAuthorBySlug('john-doe');
console.log(author.getBio());
console.log(author.getProfileImage());
await author.update({
bio: 'Updated bio information',
website: 'https://johndoe.com'
});
Manage your Ghost site members (requires Ghost membership features):
const members = await ghost.getMembers({ limit: 100 });
members.forEach(member => {
console.log(`${member.getName()} - ${member.getEmail()}`);
console.log(`Status: ${member.getStatus()}`);
});
const gmailMembers = await ghost.getMembers({
filter: '*@gmail.com'
});
const member = await ghost.getMemberByEmail('user@example.com');
console.log(member.getStatus());
console.log(member.getLabels());
const newMember = await ghost.createMember({
email: 'newuser@example.com',
name: 'New User',
note: 'VIP member'
});
await member.update({
name: 'Updated Name',
note: 'Premium member'
});
await member.delete();
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.
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
});
The SyncedInstance class enables you to synchronize content across multiple Ghost instances - perfect for staging environments, multi-region deployments, or content distribution.
Key Features:
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.
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();
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}`);
});
});
const reports = await synced.syncAll({
types: ['tags', 'posts', 'pages'],
syncOptions: {
dryRun: false
}
});
reports.forEach(report => {
console.log(`${report.contentType}: ${report.totalItems} items`);
});
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();
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);
Ghost enforces rate limits on API requests (~100 requests per IP per hour for Admin API). 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: Use pagination efficiently
const posts = await ghost.getPosts({ limit: 15 });
// β
Good: Filter on the server side
const featuredPosts = await ghost.getPosts({ featured: true, limit: 10 });
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
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`);
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);
}
| Method | Description | Returns |
|---|---|---|
getPosts(options?) | Get all posts with optional filtering | 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 | Promise<Post[]> |
getRelatedPosts(postId, limit) | Get related posts | Promise<Post[]> |
bulkUpdatePosts(ids, updates) | Update multiple posts | Promise<Post[]> |
bulkDeletePosts(ids) | Delete multiple posts | Promise<void> |
getPages(options?) | Get all pages | 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) | 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 | Promise<Author[]> |
getAuthorById(id) | Get author by ID | Promise<Author> |
getAuthorBySlug(slug) | Get author by slug | Promise<Author> |
getMembers(options?) | Get all members | 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> |
| 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> |
| 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> |
| 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> |
| 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> |
| 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> |
| 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 |
pnpm test
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';
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.
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.
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.
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.
FAQs
An unofficial Ghost CMS API package enabling content and admin functionality for managing posts.
The npm package @apiclient.xyz/ghost receives a total of 8 weekly downloads. As such, @apiclient.xyz/ghost popularity was classified as not popular.
We found that @apiclient.xyz/ghost demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Product
Reachability analysis for Ruby is now in beta, helping teams identify which vulnerabilities are truly exploitable in their applications.

Research
/Security News
Malicious npm packages use Adspect cloaking and fake CAPTCHAs to fingerprint visitors and redirect victims to crypto-themed scam sites.

Security News
Recent coverage mislabels the latest TEA protocol spam as a worm. Hereβs whatβs actually happening.