import type { WP_Post_Status_Name, WP_REST_API_Category, WP_REST_API_Post, WP_REST_API_Tag, } from 'wp-types'; import { Cms, CmsClient } from '@kit/cms-types'; import GetTagsOptions = Cms.GetTagsOptions; /** * Creates a new WordpressClient instance. * * @param {string} apiUrl - The URL of the Wordpress API. * @returns {WordpressClient} A new WordpressClient instance. */ export function createWordpressClient( apiUrl = process.env.WORDPRESS_API_URL as string, ) { return new WordpressClient(apiUrl); } /** * @name WordpressClient * @description Represents a client for interacting with a Wordpress CMS. * Implements the CmsClient interface. */ class WordpressClient implements CmsClient { constructor(private readonly apiUrl: string) {} /** * Retrieves content items from a CMS based on the provided options. * * @param {Cms.GetContentItemsOptions} options - The options to customize the retrieval of content items. */ async getContentItems(options: Cms.GetContentItemsOptions) { const queryParams = new URLSearchParams({ _embed: 'true', }); if (options?.limit && options.limit !== Infinity) { queryParams.append('per_page', options.limit.toString()); } if (options?.offset) { queryParams.append('offset', options.offset.toString()); } if (options.sortBy) { const sortBy = mapSortByParam(options.sortBy); if (sortBy) { queryParams.append('orderby', sortBy); } } if (options.sortDirection) { queryParams.append('order', options.sortDirection); } if (options?.categories) { const ids = await this.getCategories({ slugs: options.categories, }).then((categories) => categories.map((category) => category.id)); if (ids.length) { queryParams.append('categories', ids.join(',')); } else { console.warn( 'No categories found for the provided slugs', options.categories, ); } } if (options?.tags) { const allTags = [...options.tags, options.language ?? ''].filter(Boolean); const ids = await this.getTags({ slugs: allTags, }).then((tags) => tags.map((tag) => tag.id)); if (ids.length) { queryParams.append('tags', ids.join(',')); } else { console.warn('No tags found for the provided slugs', options.tags); } } if (options?.parentIds && options.parentIds.length > 0) { queryParams.append('parent', options.parentIds.join(',')); } const endpoints = [ `/wp-json/wp/v2/posts?${queryParams.toString()}`, `/wp-json/wp/v2/pages?${queryParams.toString()}`, ]; const endpoint = options.collection === 'posts' ? endpoints[0] : endpoints[1]; const url = `${this.apiUrl}${endpoint}`; const response = await fetch(url); const totalHeader = response.headers.get('X-WP-Total'); const total = totalHeader ? Number(totalHeader) : 0; const results = (await response.json()) as WP_REST_API_Post[]; const status = options.status ?? 'published'; const postsResults = await Promise.allSettled( results.map(async (item: WP_REST_API_Post) => { let parentId: string | undefined; if (!item) { throw new Error('Failed to fetch content items'); } if (item.parent) { parentId = item.parent.toString(); } const mappedStatus = mapToStatus(item.status as WP_Post_Status_Name); if (mappedStatus !== status) { throw new Error('Status does not match'); } const categories = await this.getCategoriesByIds(item.categories ?? []); const tags = await this.getTagsByIds(item.tags ?? []); const image = item.featured_media ? this.getFeaturedMedia(item) : ''; const order = item.menu_order ?? 0; return { id: item.id.toString(), title: item.title.rendered, label: item.title.rendered, content: item.content.rendered, description: item.excerpt.rendered, image, url: item.link, slug: item.slug, publishedAt: item.date, status: mappedStatus ?? 'draft', categories: categories, tags: tags, parentId, order, children: [], }; }), ); const posts = postsResults .filter((item) => item.status === 'fulfilled') .map((item) => item.value); return { total, items: posts, }; } async getContentItemBySlug({ slug, collection, status, }: { slug: string; collection: string; status?: Cms.ContentItemStatus; }) { const searchParams = new URLSearchParams({ _embed: 'true', slug, }); const endpoints = [ `/wp-json/wp/v2/posts?${searchParams.toString()}`, `/wp-json/wp/v2/pages?${searchParams.toString()}`, ]; const endpoint = collection === 'posts' ? endpoints[0] : endpoints[1]; const responses = await fetch(this.apiUrl + endpoint).then( (res) => res.json() as Promise, ); const item = responses[0]; if (!item) { return; } const mappedStatus = status ? mapToStatus(item.status as WP_Post_Status_Name) : undefined; const statusMatch = status ? mappedStatus === status : true; if (!statusMatch) { return; } const categories = await this.getCategoriesByIds(item.categories ?? []); const tags = await this.getTagsByIds(item.tags ?? []); const image = item.featured_media ? this.getFeaturedMedia(item) : ''; return { id: item.id.toString(), image, order: item.menu_order ?? 0, url: item.link, description: item.excerpt.rendered, children: [], title: item.title.rendered, label: item.title.rendered, content: item.content.rendered, slug: item.slug, publishedAt: item.date, status: mappedStatus ?? 'draft', categories, tags, parentId: item.parent?.toString(), }; } async getCategoryBySlug(slug: string) { const url = `${this.apiUrl}/wp-json/wp/v2/categories?slug=${slug}`; const response = await fetch(url); const data = await response.json(); if (data.length === 0) { return; } const item = data[0] as WP_REST_API_Category; return { id: item.id.toString(), name: item.name, slug: item.slug, }; } async getTagBySlug(slug: string) { const url = `${this.apiUrl}/wp-json/wp/v2/tags?slug=${slug}`; const response = await fetch(url); const data = await response.json(); if (data.length === 0) { return; } const item = data[0] as WP_REST_API_Tag; return { id: item.id.toString(), name: item.name, slug: item.slug, }; } async getCategories(options?: Cms.GetCategoriesOptions) { const queryParams = new URLSearchParams(); if (options?.limit) { queryParams.append('per_page', options.limit.toString()); } if (options?.offset) { queryParams.append('offset', options.offset.toString()); } if (options?.slugs) { const slugs = options.slugs.join(','); queryParams.append('slug', slugs); } const response = await fetch( `${this.apiUrl}/wp-json/wp/v2/categories?${queryParams.toString()}`, ); if (!response.ok) { console.error('Failed to fetch categories', await response.json()); throw new Error('Failed to fetch categories'); } const data = (await response.json()) as WP_REST_API_Category[]; return data.map((item) => ({ id: item.id.toString(), name: item.name, slug: item.slug, })); } async getTags(options: GetTagsOptions) { const queryParams = new URLSearchParams(); if (options?.limit) { queryParams.append('per_page', options.limit.toString()); } if (options?.offset) { queryParams.append('offset', options.offset.toString()); } if (options?.slugs) { const slugs = options.slugs.join(','); queryParams.append('slug', slugs); } const response = await fetch( `${this.apiUrl}/wp-json/wp/v2/tags?${queryParams.toString()}`, ); if (!response.ok) { console.error('Failed to fetch tags', await response.json()); throw new Error('Failed to fetch tags'); } const data = (await response.json()) as WP_REST_API_Tag[]; return data.map((item) => ({ id: item.id.toString(), name: item.name, slug: item.slug, })); } private async getTagsByIds(ids: number[]) { const promises = ids.map((id) => fetch(`${this.apiUrl}/wp-json/wp/v2/tags/${id}`), ); const responses = await Promise.all(promises); const data = (await Promise.all( responses.map((response) => response.json()), )) as WP_REST_API_Tag[]; return data.map((item) => ({ id: item.id.toString(), name: item.name, slug: item.slug, })); } private async getCategoriesByIds(ids: number[]) { const promises = ids.map((id) => fetch(`${this.apiUrl}/wp-json/wp/v2/categories/${id}`), ); const responses = await Promise.all(promises); const data = (await Promise.all( responses.map((response) => response.json()), )) as WP_REST_API_Category[]; return data.map((item) => ({ id: item.id.toString(), name: item.name, slug: item.slug, })); } private getFeaturedMedia(post: WP_REST_API_Post) { const embedded = post._embedded ?? { 'wp:featuredmedia': [], }; const media = embedded['wp:featuredmedia'] ?? []; const item = media?.length > 0 ? media[0] : null; return item ? ( item as { source_url: string; } ).source_url : ''; } } function mapSortByParam(sortBy: string) { switch (sortBy) { case 'publishedAt': return 'date'; case 'title': return 'title'; case 'slug': return 'slug'; case 'order': return 'menu_order'; default: return; } } function mapToStatus(status: WP_Post_Status_Name): Cms.ContentItemStatus { const Draft = 'draft' as WP_Post_Status_Name; const Publish = 'publish' as WP_Post_Status_Name; const Pending = 'pending' as WP_Post_Status_Name; switch (status) { case Draft: return 'draft'; case Publish: return 'published'; case Pending: return 'pending'; default: return 'draft'; } }