Creating a Custom CMS Client
Build your own CMS client to connect Sanity, Strapi, Contentful, Payload CMS, or any headless CMS to Makerkit.
Create a custom CMS client to connect any headless CMS to Makerkit. Extend the CmsClient abstract class, implement the required methods to fetch content from your CMS API, and register your client. Your custom implementation plugs into Makerkit's existing blog, documentation, and content pages without modifying application code.
A custom CMS client is a class that extends CmsClient and implements the required methods to fetch content from a third-party CMS API.
- Build a custom client when: you're using Sanity, Strapi, Contentful, Payload CMS, or any headless CMS not included by default.
- Use the existing clients when: Keystatic or WordPress meet your needs - no custom code required.
This page is part of the CMS Integration documentation.
The CmsClient Interface
Your custom client must extend CmsClient and implement these abstract methods:
import { CmsClient, Cms } from '@kit/cms-types';export abstract class CmsClient { // Fetch paginated content items abstract getContentItems( options?: Cms.GetContentItemsOptions ): Promise<{ total: number; items: Cms.ContentItem[] }>; // Fetch a single content item by slug abstract getContentItemBySlug(params: { slug: string; collection: string; }): Promise<Cms.ContentItem | undefined>; // Fetch categories abstract getCategories( options?: Cms.GetCategoriesOptions ): Promise<Cms.Category[]>; // Fetch a single category by slug abstract getCategoryBySlug( slug: string ): Promise<Cms.Category | undefined>; // Fetch tags abstract getTags( options?: Cms.GetTagsOptions ): Promise<Cms.Tag[]>; // Fetch a single tag by slug abstract getTagBySlug( slug: string ): Promise<Cms.Tag | undefined>;}The full interface is defined at packages/cms/types/src/cms-client.ts.
Example: HTTP API Client
Here's a complete implementation for a custom HTTP API:
import { CmsClient, Cms } from '@kit/cms-types';export class CustomApiClient extends CmsClient { private baseUrl: string; constructor(baseUrl: string) { super(); this.baseUrl = baseUrl; } async getContentItems( options?: Cms.GetContentItemsOptions ): Promise<{ total: number; items: Cms.ContentItem[] }> { const response = await fetch(`${this.baseUrl}/content`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options ?? {}), }); if (!response.ok) { throw new Error(`CMS API error: ${response.status}`); } return response.json(); } async getContentItemBySlug(params: { slug: string; collection: string; }): Promise<Cms.ContentItem | undefined> { const response = await fetch( `${this.baseUrl}/content/${params.collection}/${params.slug}` ); if (response.status === 404) { return undefined; } if (!response.ok) { throw new Error(`CMS API error: ${response.status}`); } return response.json(); } async getCategories( options?: Cms.GetCategoriesOptions ): Promise<Cms.Category[]> { const response = await fetch(`${this.baseUrl}/categories`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options ?? {}), }); if (!response.ok) { throw new Error(`CMS API error: ${response.status}`); } return response.json(); } async getCategoryBySlug(slug: string): Promise<Cms.Category | undefined> { const response = await fetch(`${this.baseUrl}/categories/${slug}`); if (response.status === 404) { return undefined; } if (!response.ok) { throw new Error(`CMS API error: ${response.status}`); } return response.json(); } async getTags(options?: Cms.GetTagsOptions): Promise<Cms.Tag[]> { const response = await fetch(`${this.baseUrl}/tags`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options ?? {}), }); if (!response.ok) { throw new Error(`CMS API error: ${response.status}`); } return response.json(); } async getTagBySlug(slug: string): Promise<Cms.Tag | undefined> { const response = await fetch(`${this.baseUrl}/tags/${slug}`); if (response.status === 404) { return undefined; } if (!response.ok) { throw new Error(`CMS API error: ${response.status}`); } return response.json(); }}Registering Your Client
Create a factory function that returns your client:
// packages/cms/custom/src/index.tsimport { CustomApiClient } from './custom-api-client';export function createCustomCmsClient() { const baseUrl = process.env.CUSTOM_CMS_API_URL; if (!baseUrl) { throw new Error('CUSTOM_CMS_API_URL environment variable is required'); } return new CustomApiClient(baseUrl);}Then update the CMS client factory at packages/cms/core/src/create-cms-client.ts:
import { createCustomCmsClient } from '@kit/cms-custom';export async function createCmsClient() { const cmsClient = process.env.CMS_CLIENT ?? 'keystatic'; switch (cmsClient) { case 'keystatic': return createKeystaticClient(); case 'wordpress': return createWordpressClient(); case 'custom': return createCustomCmsClient(); default: throw new Error(`Unknown CMS client: ${cmsClient}`); }}Using CMS SDKs
For established CMS platforms, use their official SDKs:
Sanity Example
import { createClient } from '@sanity/client';import { CmsClient, Cms } from '@kit/cms-types';export class SanityClient extends CmsClient { private client; constructor() { super(); this.client = createClient({ projectId: process.env.SANITY_PROJECT_ID!, dataset: process.env.SANITY_DATASET!, useCdn: true, }); } async getContentItems( options?: Cms.GetContentItemsOptions ): Promise<{ total: number; items: Cms.ContentItem[] }> { const { collection = 'post', limit = 10, offset = 0 } = options ?? {}; const query = `{ "items": *[_type == $type] | order(publishedAt desc) [$start...$end] { "id": _id, "slug": slug.current, title, description, "publishedAt": publishedAt, "content": body }, "total": count(*[_type == $type]) }`; const result = await this.client.fetch(query, { type: collection, start: offset, end: offset + limit, }); return result; } // ... implement remaining methods}Common Pitfalls
- Using
implementsinstead ofextends:CmsClientis an abstract class, not an interface. Useextends CmsClient, notimplements CmsClient. - Forgetting error handling: CMS APIs fail. Wrap fetch calls in try/catch and handle network errors, rate limits, and authentication failures.
- Not returning
undefinedfor missing items:getContentItemBySlug()should returnundefined(not throw) when an item doesn't exist. Callers expect to check for undefined. - Ignoring pagination parameters: Implement
limitandoffsetproperly. Returning all items whenlimit: 10is passed breaks pagination. - Hardcoding API URLs: Use environment variables for API URLs and credentials. Never commit secrets to your repository.
- Forgetting to register the client: After creating your class, update the factory function in
create-cms-client.tsand add your CMS type to the switch statement.
Frequently Asked Questions
Which CMS platforms can I integrate?
Do I need to implement all methods?
How do I handle authentication?
Can I use GraphQL instead of REST?
How do I test my custom client?
Back to: CMS Integration →