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

This commit is contained in:
2025-10-10 12:57:31 +00:00
parent 719bfafb93
commit 11a9b23802
14 changed files with 875 additions and 125 deletions

View File

@@ -0,0 +1,191 @@
/**
* 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');
}
};
}