---
status: "published"
label: "CMS API"
title: "CMS API Reference for the Next.js Supabase SaaS Kit"
description: "Complete API reference for fetching, filtering, and rendering content from any CMS provider in Makerkit."
order: 1
---
The CMS API provides a unified interface for fetching content regardless of your storage backend. The same code works with Keystatic, WordPress, Supabase, or any custom CMS client you create.
## Creating a CMS Client
The `createCmsClient` function returns a client configured for your chosen provider:
```tsx
import { createCmsClient } from '@kit/cms';
const client = await createCmsClient();
```
The provider is determined by the `CMS_CLIENT` environment variable:
```bash
CMS_CLIENT=keystatic # Default
CMS_CLIENT=wordpress
CMS_CLIENT=supabase # Requires plugin
```
You can also override the provider at runtime:
```tsx
import { createCmsClient } from '@kit/cms';
// Force WordPress regardless of env var
const wpClient = await createCmsClient('wordpress');
```
## Fetching Multiple Content Items
Use `getContentItems()` to retrieve lists of content with filtering and pagination:
```tsx
import { createCmsClient } from '@kit/cms';
const client = await createCmsClient();
const { items, total } = await client.getContentItems({
collection: 'posts',
limit: 10,
offset: 0,
sortBy: 'publishedAt',
sortDirection: 'desc',
status: 'published',
});
```
### Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `collection` | `string` | Required | The collection to query (`posts`, `documentation`, `changelog`) |
| `limit` | `number` | `10` | Maximum items to return |
| `offset` | `number` | `0` | Number of items to skip (for pagination) |
| `sortBy` | `'publishedAt' \| 'order' \| 'title'` | `'publishedAt'` | Field to sort by |
| `sortDirection` | `'asc' \| 'desc'` | `'asc'` | Sort direction |
| `status` | `'published' \| 'draft' \| 'review' \| 'pending'` | `'published'` | Filter by content status |
| `categories` | `string[]` | - | Filter by category slugs |
| `tags` | `string[]` | - | Filter by tag slugs |
| `language` | `string` | - | Filter by language code |
| `content` | `boolean` | `true` | Whether to fetch full content (set `false` for list views) |
| `parentIds` | `string[]` | - | Filter by parent content IDs (for hierarchical content) |
### Pagination Example
```tsx
import { createCmsClient } from '@kit/cms';
import { cache } from 'react';
const getPostsPage = cache(async (page: number, perPage = 10) => {
const client = await createCmsClient();
return client.getContentItems({
collection: 'posts',
limit: perPage,
offset: (page - 1) * perPage,
sortBy: 'publishedAt',
sortDirection: 'desc',
});
});
// Usage in a Server Component
async function BlogList({ page }: { page: number }) {
const { items, total } = await getPostsPage(page);
const totalPages = Math.ceil(total / 10);
return (
{items.map((post) => (
{post.title}
{post.description}
))}
);
}
```
### Filtering by Category
```tsx
const { items } = await client.getContentItems({
collection: 'posts',
categories: ['tutorials', 'guides'],
limit: 5,
});
```
### List View Optimization
For list views where you only need titles and descriptions, skip content fetching:
```tsx
const { items } = await client.getContentItems({
collection: 'posts',
content: false, // Don't fetch full content
limit: 20,
});
```
## Fetching a Single Content Item
Use `getContentItemBySlug()` to retrieve a specific piece of content:
```tsx
import { createCmsClient } from '@kit/cms';
const client = await createCmsClient();
const post = await client.getContentItemBySlug({
slug: 'getting-started',
collection: 'posts',
});
if (!post) {
// Handle not found
}
```
### Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `slug` | `string` | Required | The URL slug of the content item |
| `collection` | `string` | Required | The collection to search |
| `status` | `'published' \| 'draft' \| 'review' \| 'pending'` | `'published'` | Required status for the item |
### Draft Preview
To preview unpublished content (e.g., for admin users):
```tsx
const draft = await client.getContentItemBySlug({
slug: 'upcoming-feature',
collection: 'posts',
status: 'draft',
});
```
## Content Item Shape
All CMS providers return items matching this TypeScript interface:
```tsx
interface ContentItem {
id: string;
title: string;
label: string | undefined;
slug: string;
url: string;
description: string | undefined;
content: unknown; // Provider-specific format
publishedAt: string; // ISO date string
image: string | undefined;
status: 'draft' | 'published' | 'review' | 'pending';
categories: Category[];
tags: Tag[];
order: number;
parentId: string | undefined;
children: ContentItem[];
collapsible?: boolean;
collapsed?: boolean;
}
interface Category {
id: string;
name: string;
slug: string;
}
interface Tag {
id: string;
name: string;
slug: string;
}
```
## Rendering Content
Content format varies by provider (Markdoc nodes, HTML, React nodes). Use the `ContentRenderer` component for provider-agnostic rendering:
```tsx
import { createCmsClient, ContentRenderer } from '@kit/cms';
import { notFound } from 'next/navigation';
async function ArticlePage({ slug }: { slug: string }) {
const client = await createCmsClient();
const article = await client.getContentItemBySlug({
slug,
collection: 'posts',
});
if (!article) {
notFound();
}
return (
{article.title}
{article.description &&
{article.description}
}
);
}
```
## Working with Categories and Tags
### Fetch All Categories
```tsx
const categories = await client.getCategories({
limit: 50,
offset: 0,
});
```
### Fetch a Category by Slug
```tsx
const category = await client.getCategoryBySlug('tutorials');
if (category) {
// Fetch posts in this category
const { items } = await client.getContentItems({
collection: 'posts',
categories: [category.slug],
});
}
```
### Fetch All Tags
```tsx
const tags = await client.getTags({
limit: 100,
});
```
### Fetch a Tag by Slug
```tsx
const tag = await client.getTagBySlug('react');
```
## Building Dynamic Pages
### Blog Post Page
```tsx {% title="app/[locale]/(marketing)/blog/[slug]/page.tsx" %}
import { createCmsClient, ContentRenderer } from '@kit/cms';
import { notFound } from 'next/navigation';
interface Props {
params: Promise<{ slug: string }>;
}
export async function generateStaticParams() {
const client = await createCmsClient();
const { items } = await client.getContentItems({
collection: 'posts',
content: false,
limit: 1000,
});
return items.map((post) => ({
slug: post.slug,
}));
}
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
const client = await createCmsClient();
const post = await client.getContentItemBySlug({
slug,
collection: 'posts',
});
if (!post) {
return {};
}
return {
title: post.title,
description: post.description,
openGraph: {
images: post.image ? [post.image] : [],
},
};
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params;
const client = await createCmsClient();
const post = await client.getContentItemBySlug({
slug,
collection: 'posts',
});
if (!post) {
notFound();
}
return (
{post.title}
);
}
```
### CMS-Powered Static Pages
Store pages like Terms of Service or Privacy Policy in your CMS:
```tsx {% title="app/[slug]/page.tsx" %}
import { createCmsClient, ContentRenderer } from '@kit/cms';
import { notFound } from 'next/navigation';
interface Props {
params: Promise<{ slug: string }>;
}
export default async function StaticPage({ params }: Props) {
const { slug } = await params;
const client = await createCmsClient();
const page = await client.getContentItemBySlug({
slug,
collection: 'pages', // Create this collection in your CMS
});
if (!page) {
notFound();
}
return (
{page.title}
);
}
```
{% alert type="default" title="Create the pages collection" %}
This example assumes you've added a `pages` collection to your CMS configuration. By default, Makerkit includes `posts`, `documentation`, and `changelog` collections.
{% /alert %}
## Caching Strategies
### React Cache
Wrap CMS calls with React's `cache()` for request deduplication:
```tsx
import { createCmsClient } from '@kit/cms';
import { cache } from 'react';
export const getPost = cache(async (slug: string) => {
const client = await createCmsClient();
return client.getContentItemBySlug({
slug,
collection: 'posts',
});
});
```
### Next.js Data Cache
The CMS client respects Next.js caching. For static content, pages are cached at build time with `generateStaticParams()`.
For dynamic content that should revalidate:
```tsx
import { unstable_cache } from 'next/cache';
import { createCmsClient } from '@kit/cms';
const getCachedPosts = unstable_cache(
async () => {
const client = await createCmsClient();
return client.getContentItems({ collection: 'posts', limit: 10 });
},
['posts-list'],
{ revalidate: 3600 } // Revalidate every hour
);
```
## Provider-Specific Notes
### Keystatic
- Collections: `posts`, `documentation`, `changelog` (configurable in `keystatic.config.ts`)
- Categories and tags are stored as arrays of strings
- Content is Markdoc, rendered via `@kit/keystatic/renderer`
### WordPress
- Collections map to WordPress content types: use `posts` for posts, `pages` for pages
- Categories and tags use WordPress's native taxonomy system
- Language filtering uses tags (add `en`, `de`, etc. tags to posts)
- Content is HTML, rendered via `@kit/wordpress/renderer`
### Supabase
- Uses the `content_items`, `categories`, and `tags` tables
- Requires the Supabase CMS plugin installation
- Content can be HTML or any format you store
- Works with [Supamode](/supabase-cms) for admin UI
## Next Steps
- [Keystatic Setup](/docs/next-supabase-turbo/content/keystatic): Configure local or GitHub storage
- [WordPress Setup](/docs/next-supabase-turbo/content/wordpress): Connect to WordPress REST API
- [Supabase CMS Plugin](/docs/next-supabase-turbo/content/supabase): Store content in your database
- [Custom CMS Client](/docs/next-supabase-turbo/content/creating-your-own-cms-client): Build integrations for Sanity, Contentful, etc.