# @apiclient.xyz/ghost

an unofficial ghost api package

# 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](#-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: '<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:

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

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

### Update Post

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

### 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: '<p>Contact information...</p>'
});

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

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

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

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<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>` |

### 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

```bash
pnpm test
```

## 📝 TypeScript Support

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

```typescript
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:

```typescript
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](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](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.

# changelog.md for @apiclient.xyz/ghost

## 2025-10-11 - 2.2.1 - fix(syncedinstance)
Prevent same-instance syncs and sanitize post update payloads; update tests and docs

- SyncedInstance now validates and normalizes source and target base URLs (trailing slashes and case) and throws a clear error when attempting to sync an instance to itself to prevent circular syncs.
- Post.update signature changed to accept Partial<IPost>. Update logic now builds a sanitized payload and removes read-only/computed fields (uuid, comment_id, url, excerpt, reading_time, created_at, primary_author, primary_tag, etc.) before calling the Admin API to avoid conflicts.
- Added/updated integration tests (dates, syncedinstance validation) and adjusted tag tests to be resilient; README expanded with examples, usage notes, and multi-instance sync safety details.
- Improved tag sync/update to preserve slug when updating tags on targets.

## 2025-10-10 - 2.2.0 - feat(apiclient)
Add native Admin & Content API clients, JWT generator, and tag visibility features; remove external @tryghost deps and update docs

- Add native GhostAdminAPI and GhostContentAPI implementations (ts/apiclient/*) using global fetch with support for browse/read/add/edit/delete, webhooks, and image upload.
- Add generateToken JWT implementation compatible with Ghost Admin API (ts/apiclient/ghost.jwt.ts) with Web Crypto and Node fallbacks.
- Switch Ghost class to use built-in apiclient implementations and bump default API version to v6.0.
- Use Admin API to fetch ALL tags (including zero-count) and add visibility filtering and minimatch filtering in getTags; add convenience methods getPublicTags and getInternalTags.
- Add Tag methods getVisibility(), isInternal(), and isPublic() to expose visibility information.
- Replace imports of @tryghost/* with local apiclient implementations in ts/ghost.plugins.ts and remove @tryghost dependencies from package.json.
- Update README to document tag visibility, new tag-related APIs/examples, and other notes about using Admin API for tags.

## 2025-10-08 - 2.1.0 - feat(syncedinstance)
Add SyncedInstance for multi-instance content synchronization, export it, add tests, and expand README

- Introduce SyncedInstance class (ts/classes.syncedinstance.ts) to synchronize tags, posts and pages across Ghost instances with mapping and history support. New APIs: syncTags, syncPosts, syncPages, syncAll, getSyncStatus, clearSyncHistory, clearMappings.
- Export SyncedInstance from the package entry point (ts/index.ts).
- Add integration tests for SyncedInstance (test/test.syncedinstance.node.ts).
- Expand README (readme.md) with Quick Start, detailed Multi-Instance Synchronization docs, examples and updated API reference.

## 2025-10-08 - 2.0.0 - BREAKING CHANGE(classes.ghost)
Remove Settings and Webhooks browse/read APIs, remove noisy console.error logs, and update tests/docs

- Removed Settings API methods from Ghost: getSettings and updateSettings (breaking change).
- Removed Webhooks browsing/reading methods from Ghost: getWebhooks and getWebhookById. createWebhook, updateWebhook and deleteWebhook remain.
- Removed test/test.settings.node.ts and simplified test/test.webhook.node.ts to only exercise create/update/delete webhook flows without feature-availability guarding.
- Stripped console.error debug logging across multiple classes (Author, Ghost, Member, Page, Post, Tag) to reduce noisy runtime output.
- Updated README: removed 'Site Settings' section and clarified webhook API limitations supported by the underlying Ghost Admin SDK.

## 2025-10-07 - 1.4.1 - fix(tests)
Remove updated_at from post and page update test payloads

- Stop setting updated_at in test update payloads to avoid mutating server-managed timestamps
- Changed test/test.post.node.ts: removed updated_at assignment when updating a post
- Changed test/test.page.node.ts: removed updated_at assignment when updating a page

## 2025-10-07 - 1.4.0 - feat(classes.ghost)
Add members, settings and webhooks support; implement Member class and add tests

- Introduce IMember and Member class (ts/classes.member.ts) with CRUD (update, delete) and JSON helpers
- Add member management API to Ghost: getMembers (with minimatch filtering), getMemberById, getMemberByEmail, createMember
- Add site settings API to Ghost: getSettings and updateSettings (admin API wrappers)
- Add webhooks management to Ghost: getWebhooks, getWebhookById, createWebhook, updateWebhook, deleteWebhook
- Wire smartmatch filtering for members using @push.rocks/smartmatch
- Export Member from ts/index.ts so it's part of the public API
- Add comprehensive node tests for Ghost, Author, Member, Page, Post, Tag, Settings and Webhooks (test/*.node.ts)
- Fix Post construction usages in classes.ghost to pass the Ghost instance when creating Post objects

## 2025-10-07 - 1.3.0 - feat(core)
Add Ghost CMS API client classes and README documentation

- Implemented core TypeScript classes: Ghost, Post, Tag, Author and Page with CRUD methods and helpers
- Added TypeScript interfaces for posts, tags, authors and pages to provide typings
- Included comprehensive README with installation, usage examples and API method documentation
- Added unit test scaffold under test/test.ts
- Updated package metadata and commitinfo (version 1.2.0)

## 2025-10-07 - 1.2.0 - feat(ghost)
Implement Tag, Author and Page models; add advanced filtering, search, bulk operations, image upload, related-posts, update tests and bump dependencies

- Add fully implemented Author, Tag and Page classes with CRUD methods
- Enhance Ghost class with: improved getPosts filtering (tag/author/featured/custom filters), getTags/getAuthors/getPages with minimatch filtering, getTag/getAuthor/getPage by id/slug, createTag/createPage, searchPosts, uploadImage, bulkUpdatePosts, bulkDeletePosts and getRelatedPosts
- Refactor Post types (IPost) and update Post class to use the new type and consistent constructors/serialization
- Export new modules (author, tag, page) from index.ts
- Integrate @push.rocks/smartmatch for pattern filtering and expose it via ghost.plugins
- Update tests to exercise tags, authors, pages, filtering, search and related posts; change test baseUrl to localhost and adjust imports
- Bump devDependencies and dependencies versions, adjust test script, add packageManager metadata and add pnpm-workspace.yaml

## 2024-07-06 - 1.1.0 - feat(core)
Enhanced post fetching and creation with additional metadata and support for HTML source

- Added support for fetching posts with included tags and authors.
- Implemented helper method to create posts directly from HTML content.
- Enhanced Post class to include additional metadata such as visibility, created_at, updated_at, published_at, etc.
- Modified getPosts method in Ghost class to include tags and authors in the response.

## 2024-07-01 - 1.0.3 - fix(docs)
Updated the project keywords and readme content for better clarity and SEO

- Improved the project description in `package.json` and `npmextra.json`.
- Added comprehensive usage instructions and examples in `readme.md`.

## 2024-07-01 - 1.0.2 - fix(core)
No changes in the project files


## 2024-07-01 - 1.0.1 - Initial Release
Initial release of the project.