/** * Ghost Content API Client * Read-only API for published content using native fetch */ import type { IBrowseOptions, IReadOptions, IGhostAPIResponse, IGhostErrorResponse } from './ghost.types.js'; export interface IGhostContentAPIOptions { url: string; key: string; version?: string; ghostPath?: string; } export class GhostContentAPI { private url: string; private key: string; private version: string; private ghostPath: string; constructor(options: IGhostContentAPIOptions) { this.url = options.url.replace(/\/$/, ''); // Remove trailing slash this.key = options.key; this.version = options.version || 'v3'; this.ghostPath = options.ghostPath || 'ghost'; if (!this.url) { throw new Error('GhostContentAPI: url is required'); } if (!this.key) { throw new Error('GhostContentAPI: key is required'); } } /** * Build the API prefix based on version */ private getAPIPrefix(): string { // v5+ doesn't need version prefix if (this.version === 'v5' || this.version === 'v6' || this.version.match(/^v[5-9]\.\d+/)) { return `/content/`; } // v2-v4 and canary need version prefix return `/${this.version}/content/`; } /** * Build full API URL */ private buildUrl(resource: string, identifier?: string, params?: Record): string { const apiPrefix = this.getAPIPrefix(); let endpoint = `${this.url}/${this.ghostPath}/api${apiPrefix}${resource}/`; if (identifier) { endpoint += `${identifier}/`; } // Add key to params const queryParams = { key: this.key, ...params }; // Build query string const queryString = Object.keys(queryParams) .filter(key => queryParams[key] !== undefined && queryParams[key] !== null) .map(key => { const value = Array.isArray(queryParams[key]) ? queryParams[key].join(',') : queryParams[key]; return `${key}=${encodeURIComponent(value)}`; }) .join('&'); return queryString ? `${endpoint}?${queryString}` : endpoint; } /** * Make API request */ private async makeRequest( resource: string, identifier?: string, params?: Record ): Promise { const url = this.buildUrl(resource, identifier, params); try { const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Accept-Version': this.version.match(/^\d+\.\d+$/) ? `v${this.version}` : this.version, 'User-Agent': 'GhostContentAPI/2.0' } }); if (!response.ok) { const errorData: IGhostErrorResponse = await response.json().catch(() => ({ errors: [{ type: 'UnknownError', message: response.statusText }] })); const error = errorData.errors?.[0]; const err = new Error(error?.message || response.statusText); Object.assign(err, { name: error?.type || 'GhostContentAPIError', statusCode: response.status, ...error }); throw err; } const data: IGhostAPIResponse = await response.json(); // Extract the resource data const resourceData = data[resource]; if (!resourceData) { throw new Error(`Response missing ${resource} property`); } // If it's an array and has meta, attach meta to the array if (Array.isArray(resourceData) && data.meta) { return Object.assign(resourceData, { meta: data.meta }); } // If it's an array with single item and no meta, return the item if (Array.isArray(resourceData) && resourceData.length === 1 && !data.meta) { return resourceData[0]; } return resourceData as T | T[]; } catch (error) { throw error; } } /** * Create resource API methods */ public posts = { browse: (options?: IBrowseOptions) => this.makeRequest('posts', undefined, options), read: (options: IReadOptions) => { if (options.slug) { return this.makeRequest('posts', `slug/${options.slug}`, options); } if (options.id) { return this.makeRequest('posts', options.id, options); } throw new Error('Must provide id or slug'); } }; public pages = { browse: (options?: IBrowseOptions) => this.makeRequest('pages', undefined, options), read: (options: IReadOptions) => { if (options.slug) { return this.makeRequest('pages', `slug/${options.slug}`, options); } if (options.id) { return this.makeRequest('pages', options.id, options); } throw new Error('Must provide id or slug'); } }; public tags = { browse: (options?: IBrowseOptions) => this.makeRequest('tags', undefined, options), read: (options: IReadOptions) => { if (options.slug) { return this.makeRequest('tags', `slug/${options.slug}`, options); } if (options.id) { return this.makeRequest('tags', options.id, options); } throw new Error('Must provide id or slug'); } }; public authors = { browse: (options?: IBrowseOptions) => this.makeRequest('authors', undefined, options), read: (options: IReadOptions) => { if (options.slug) { return this.makeRequest('authors', `slug/${options.slug}`, options); } if (options.id) { return this.makeRequest('authors', options.id, options); } throw new Error('Must provide id or slug'); } }; }