Read-Only SDK
Read-Only SDK
Fetch published content from frontend applications using the lightweight read-only @modulacms/sdk client.
ModulaClient
Create an instance with new ModulaClient(config):
import { ModulaClient } from '@modulacms/sdk'
const client = new ModulaClient({
baseUrl: 'https://cms.example.com',
apiKey: 'your-read-only-key',
})
All methods are async and return promises.
getPage
The primary method for frontend content delivery. Resolves a route slug to a fully rendered content tree.
const page = await client.getPage('about')
GetPageOptions
Pass an options object as the second argument:
const page = await client.getPage<MyPageType>('about', {
format: 'clean',
locale: 'fr',
validate: isMyPageType,
})
| Option | Type | Description |
|---|---|---|
format |
ContentFormat |
Output format: 'contentful', 'sanity', 'strapi', 'wordpress', 'clean', 'raw'. Overrides defaultFormat from config. |
locale |
string |
Locale code (e.g. 'en', 'fr'). Filters field values to the specified locale. |
validate |
Validator<T> |
Runtime type guard for response validation. |
Validator Pattern
The Validator<T> type is a type-narrowing function:
type Validator<T> = (data: unknown) => data is T
When you provide a validator, getPage runs it against the parsed response and throws if validation fails. This gives you runtime safety on top of compile-time types.
interface BlogPage {
root: {
datatype: { info: { label: string } }
fields: Array<{ info: { name: string }; content: { field_value: string } }>
}
}
function isBlogPage(data: unknown): data is BlogPage {
return (
typeof data === 'object' &&
data !== null &&
'root' in data
)
}
const page = await client.getPage<BlogPage>('blog', {
validate: isBlogPage,
})
// page is typed as BlogPage
Content Methods
| Method | Signature | Description |
|---|---|---|
getPage |
<T>(slug: string, options?: GetPageOptions<T>) => Promise<T> |
Fetch rendered content tree by route slug. |
listRoutes |
() => Promise<Route[]> |
List all public routes. |
getRoute |
(id: string) => Promise<Route> |
Get a single route by ID. |
listContentData |
() => Promise<ContentData[]> |
List all public content data nodes. |
getContentData |
(id: string) => Promise<ContentData> |
Get a single content data node. |
listContentFields |
() => Promise<ContentField[]> |
List all public content field values. |
getContentField |
(id: string) => Promise<ContentField> |
Get a single content field value. |
listMedia |
() => Promise<Media[]> |
List all media assets. |
getMedia |
(id: string) => Promise<Media> |
Get a single media asset. |
listMediaPaginated |
(params: PaginationParams) => Promise<PaginatedResponse<Media>> |
List media with pagination. |
listMediaDimensions |
() => Promise<MediaDimension[]> |
List media dimension presets. |
getMediaDimension |
(id: string) => Promise<MediaDimension> |
Get a single media dimension preset. |
listDatatypes |
() => Promise<Datatype[]> |
List all datatype definitions. |
getDatatype |
(id: string) => Promise<Datatype> |
Get a single datatype definition. |
listFields |
() => Promise<Field[]> |
List all field definitions. |
getField |
(id: string) => Promise<Field> |
Get a single field definition. |
queryContent |
(datatype: string, params?: QueryParams) => Promise<QueryResult> |
Query content items by datatype name. |
queryContent
Query content by datatype with filtering, sorting, and pagination:
const result = await client.queryContent('blog-post', {
sort: '-published_at',
limit: 10,
offset: 0,
locale: 'en',
status: 'published',
filters: {
category: 'news',
'price[gte]': '10',
},
})
console.log(`${result.data.length} of ${result.total} items`)
QueryParams
| Field | Type | Default | Description |
|---|---|---|---|
sort |
string |
-- | Sort field. Prefix with - for descending. |
limit |
number |
20 |
Max items to return (server max: 100). |
offset |
number |
0 |
Items to skip for pagination. |
locale |
string |
-- | Locale code for i18n content. |
status |
string |
'published' |
Content status filter. |
filters |
Record<string, string> |
-- | Field filters with optional operator suffixes. |
Filter operators: [eq], [ne], [gt], [gte], [lt], [lte], [like], [in]. A bare field name is treated as [eq].
Content Tree Structure
getPage returns a content tree in this structure (when you don't specify a format override):
type ContentTree = {
root: ContentNode
}
type ContentNode = {
datatype: {
info: Datatype // schema definition
content: ContentData // tree pointers, status, dates
}
fields: Array<{
info: Field // field definition (name, type, validation)
content: ContentField // stored value
}>
nodes?: ContentNode[] // child nodes
}
Each node pairs its schema (info) with its content instance (content). Walk nodes recursively to traverse the tree.
Framework Examples
Next.js (App Router)
import { ModulaClient } from '@modulacms/sdk'
const client = new ModulaClient({
baseUrl: process.env.CMS_URL!,
apiKey: process.env.CMS_API_KEY,
})
export default async function Page({ params }: { params: { slug: string } }) {
const page = await client.getPage(params.slug)
return <div>{JSON.stringify(page)}</div>
}
Nuxt 3
import { ModulaClient } from '@modulacms/sdk'
const client = new ModulaClient({
baseUrl: useRuntimeConfig().cmsUrl,
})
const { data: page } = await useAsyncData('page', () =>
client.getPage('home')
)
SvelteKit
import { ModulaClient } from '@modulacms/sdk'
import type { PageServerLoad } from './$types'
const client = new ModulaClient({
baseUrl: import.meta.env.CMS_URL,
})
export const load: PageServerLoad = async ({ params }) => {
const page = await client.getPage(params.slug)
return { page }
}
Error Handling
All ModulaClient methods throw ModulaError on failure. See Error Handling for details.
import { ModulaError } from '@modulacms/sdk'
try {
const page = await client.getPage('nonexistent')
} catch (err) {
if (err instanceof ModulaError) {
console.error(`API error ${err.status}: ${err.message}`)
}
}