192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
/**
|
|
* 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, any>): 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<T>(
|
|
resource: string,
|
|
identifier?: string,
|
|
params?: Record<string, any>
|
|
): Promise<T | T[]> {
|
|
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<T> = 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');
|
|
}
|
|
};
|
|
}
|