CMS API Reference for the Next.js Supabase SaaS Kit

Complete API reference for fetching, filtering, and rendering content from any CMS provider in Makerkit.

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:

import { createCmsClient } from '@kit/cms';
const client = await createCmsClient();

The provider is determined by the CMS_CLIENT environment variable:

CMS_CLIENT=keystatic # Default
CMS_CLIENT=wordpress
CMS_CLIENT=supabase # Requires plugin

You can also override the provider at runtime:

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:

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

OptionTypeDefaultDescription
collectionstringRequiredThe collection to query (posts, documentation, changelog)
limitnumber10Maximum items to return
offsetnumber0Number 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
categoriesstring[]-Filter by category slugs
tagsstring[]-Filter by tag slugs
languagestring-Filter by language code
contentbooleantrueWhether to fetch full content (set false for list views)
parentIdsstring[]-Filter by parent content IDs (for hierarchical content)

Pagination Example

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 (
<div>
{items.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.description}</p>
</article>
))}
<nav>
Page {page} of {totalPages}
</nav>
</div>
);
}

Filtering by Category

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:

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:

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

OptionTypeDefaultDescription
slugstringRequiredThe URL slug of the content item
collectionstringRequiredThe 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):

const draft = await client.getContentItemBySlug({
slug: 'upcoming-feature',
collection: 'posts',
status: 'draft',
});

Content Item Shape

All CMS providers return items matching this TypeScript interface:

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:

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>
<header>
<h1>{article.title}</h1>
{article.description && <p>{article.description}</p>}
<time dateTime={article.publishedAt}>
{new Date(article.publishedAt).toLocaleDateString()}
</time>
</header>
<ContentRenderer content={article.content} />
<footer>
{article.categories.map((cat) => (
<span key={cat.id}>{cat.name}</span>
))}
</footer>
</article>
);
}

Working with Categories and Tags

Fetch All Categories

const categories = await client.getCategories({
limit: 50,
offset: 0,
});

Fetch a Category by Slug

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

const tags = await client.getTags({
limit: 100,
});

Fetch a Tag by Slug

const tag = await client.getTagBySlug('react');

Building Dynamic Pages

Blog Post Page

app/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 (
<article>
<h1>{post.title}</h1>
<ContentRenderer content={post.content} />
</article>
);
}

CMS-Powered Static Pages

Store pages like Terms of Service or Privacy Policy in your CMS:

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 (
<div>
<h1>{page.title}</h1>
<ContentRenderer content={page.content} />
</div>
);
}

Caching Strategies

React Cache

Wrap CMS calls with React's cache() for request deduplication:

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:

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 for admin UI

Next Steps