readme.md for @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 Quick Start Core API Posts Pages Tags Authors Members Webhooks Image Upload Multi-Instance Synchronization Complete Example Performance & Best Practices Error Handling API Reference TypeScript Support Testing πŸ“¦ 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: '

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: 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 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: '

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: const post = await ghost.createPostFromHtml({ title: 'My HTML Post', html: '

Content here

' }); Update Post const post = await ghost.getPostById('post-id'); await post.update({ ...post.toJson(), title: 'Updated Title', html: '

Updated content

' }); 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())); Related Posts 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: '

Contact information...

' }); await newPage.update({ html: '

Updated content

' }); 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: '

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 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: '

Welcome

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: // βœ… 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: // βœ… 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 with optional filtering Promise getPostById(id) Get a single post by ID Promise createPost(data) Create a new post Promise createPostFromHtml(data) Create post from HTML Promise searchPosts(query, options?) Search posts by title Promise getRelatedPosts(postId, limit) Get related posts Promise bulkUpdatePosts(ids, updates) Update multiple posts Promise bulkDeletePosts(ids) Delete multiple posts Promise getPages(options?) Get all pages Promise getPageById(id) Get page by ID Promise getPageBySlug(slug) Get page by slug Promise createPage(data) Create a new page Promise getTags(options?) Get all tags (including zero-count) Promise getPublicTags(options?) Get only public tags Promise getInternalTags(options?) Get only internal tags Promise getTagById(id) Get tag by ID Promise getTagBySlug(slug) Get tag by slug Promise createTag(data) Create a new tag Promise getAuthors(options?) Get all authors Promise getAuthorById(id) Get author by ID Promise getAuthorBySlug(slug) Get author by slug Promise getMembers(options?) Get all members Promise getMemberById(id) Get member by ID Promise getMemberByEmail(email) Get member by email Promise createMember(data) Create a new member Promise createWebhook(data) Create a webhook Promise updateWebhook(id, data) Update a webhook Promise deleteWebhook(id) Delete a webhook Promise uploadImage(filePath) Upload an image Promise 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 delete() Delete the post Promise 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 delete() Delete the page Promise 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 delete() Delete the tag Promise 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 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 delete() Delete the member Promise SyncedInstance Class Method Description Returns syncPosts(options?) Sync posts to targets Promise syncPages(options?) Sync pages to targets Promise syncTags(options?) Sync tags to targets Promise syncAll(options?) Sync all content types Promise getSyncStatus() Get sync status & mappings Object clearSyncHistory() Clear sync history void clearMappings() Clear ID mappings void πŸ§ͺ Testing pnpm test πŸ“ 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 License and Legal Information 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.