# @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](#-installation) - [Quick Start](#-quick-start) - [Core API](#-core-api) - [Posts](#-posts) - [Pages](#-pages) - [Tags](#οΈ-tags) - [Authors](#-authors) - [Members](#-members) - [Webhooks](#-webhooks) - [Image Upload](#οΈ-image-upload) - [Multi-Instance Synchronization](#-multi-instance-synchronization) - [Complete Example](#-complete-example) - [Performance & Best Practices](#-performance--best-practices) - [Error Handling](#-error-handling) - [API Reference](#-api-reference) - [TypeScript Support](#-typescript-support) - [Testing](#-testing) ## π¦ Installation ```bash npm install @apiclient.xyz/ghost ``` Or with pnpm: ```bash pnpm install @apiclient.xyz/ghost ``` ## π― Quick Start ```typescript 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: '
My first post!
', 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: ```typescript 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 }); ``` ## π Posts ### Get All Posts ```typescript const posts = await ghost.getPosts(); posts.forEach(post => { console.log(post.getTitle()); console.log(post.getExcerpt()); console.log(post.getFeatureImage()); }); ``` ### Filter Posts ```typescript 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 ```typescript const post = await ghost.getPostById('post-id'); console.log(post.getTitle()); console.log(post.getHtml()); console.log(post.getAuthor()); ``` ### Create Post ```typescript const newPost = await ghost.createPost({ title: 'My Awesome Post', html: 'This is the content of my post.
', feature_image: 'https://example.com/image.jpg', tags: [{ id: 'tag-id' }], excerpt: 'A brief summary of the post' }); ``` Or create from HTML specifically: ```typescript const post = await ghost.createPostFromHtml({ title: 'My HTML Post', html: 'Content here
' }); ``` ### Update Post ```typescript const post = await ghost.getPostById('post-id'); await post.update({ ...post.toJson(), title: 'Updated Title', html: 'Updated content
' }); ``` ### Delete Post ```typescript const post = await ghost.getPostById('post-id'); await post.delete(); ``` ### Search Posts Full-text search across post titles: ```typescript const results = await ghost.searchPosts('typescript tutorial', { limit: 10 }); results.forEach(post => console.log(post.getTitle())); ``` ### Related Posts Get posts with similar tags: ```typescript 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 ```typescript 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: ```typescript const pages = await ghost.getPages(); const aboutPage = await ghost.getPageBySlug('about'); console.log(aboutPage.getHtml()); const newPage = await ghost.createPage({ title: 'Contact', html: 'Contact information...
' }); await newPage.update({ html: 'Updated content
' }); await newPage.delete(); ``` Filter pages with minimatch patterns: ```typescript const filteredPages = await ghost.getPages({ filter: 'about*', limit: 10 }); ``` ## π·οΈ Tags ### Get All Tags ```typescript // 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) ```typescript // 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 ```typescript 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 ```typescript 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 ```typescript // 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 ```typescript const authors = await ghost.getAuthors(); authors.forEach(author => { console.log(`${author.getName()} (${author.getSlug()})`); }); ``` ### Filter Authors ```typescript const filteredAuthors = await ghost.getAuthors({ filter: 'j*', limit: 10 }); ``` ### Get Single Author ```typescript const author = await ghost.getAuthorBySlug('john-doe'); console.log(author.getBio()); console.log(author.getProfileImage()); ``` ### Update Author ```typescript await author.update({ bio: 'Updated bio information', website: 'https://johndoe.com' }); ``` ## π₯ Members Manage your Ghost site members (requires Ghost membership features): ### Get Members ```typescript const members = await ghost.getMembers({ limit: 100 }); members.forEach(member => { console.log(`${member.getName()} - ${member.getEmail()}`); console.log(`Status: ${member.getStatus()}`); }); ``` ### Filter Members ```typescript const gmailMembers = await ghost.getMembers({ filter: '*@gmail.com' }); ``` ### Get Single Member ```typescript const member = await ghost.getMemberByEmail('user@example.com'); console.log(member.getStatus()); console.log(member.getLabels()); ``` ### Create Member ```typescript const newMember = await ghost.createMember({ email: 'newuser@example.com', name: 'New User', note: 'VIP member' }); ``` ### Update and Delete Members ```typescript await member.update({ name: 'Updated Name', note: 'Premium member' }); await member.delete(); ``` ## πͺ Webhooks Manage webhooks for Ghost events: ```typescript 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: ```typescript const imageUrl = await ghost.uploadImage('/path/to/image.jpg'); await ghost.createPost({ title: 'Post with Image', html: 'Content here
', 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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: ```typescript 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: 'This is an introduction to Ghost CMS...
', 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 Ghost enforces rate limits on API requests (~100 requests per IP per hour for Admin API). Keep these tips in mind: ```typescript // β 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 }); ``` ### Multi-Instance Sync Safety The library automatically prevents common pitfalls: ```typescript // β 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: ```typescript 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: ```typescript 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 with optional filtering | `Promise