Serving Your Frontend
Serving Your Frontend
Connect a frontend framework to ModulaCMS and render your content as real pages.
The pattern
Every frontend integration follows the same pattern:
- Create a catch-all route in your framework.
- Extract the slug from the URL.
- Call the ModulaCMS content delivery API with that slug.
- Handle the response: render content, follow redirects, or show a 404.
The content delivery endpoint is public and requires no authentication:
GET /api/v1/content/{slug}
Next.js (App Router)
Create a catch-all route at app/[[...slug]]/page.tsx:
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 slug = params.slug?.join('/') || 'homepage'
try {
const page = await client.getPage(slug, { format: 'clean' })
return <ContentRenderer content={page} />
} catch (err) {
if (isNotFound(err)) {
return notFound()
}
throw err
}
}
Good to know: The
[[...slug]]syntax in Next.js matches both/(no slug segments) and/about/team(multiple segments). Map the empty case to your homepage slug.
Nuxt
Create a catch-all route at pages/[...slug].vue:
<script setup lang="ts">
const route = useRoute()
const slug = (route.params.slug as string[])?.join('/') || 'homepage'
const { data: page, error } = await useFetch(
`${useRuntimeConfig().public.cmsUrl}/api/v1/content/${slug}?format=clean`
)
if (error.value?.statusCode === 404) {
throw createError({ statusCode: 404, statusMessage: 'Page Not Found' })
}
</script>
<template>
<ContentRenderer :content="page" />
</template>
SvelteKit
Create a catch-all route at src/routes/[...slug]/+page.server.ts:
import { error } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ params, fetch }) => {
const slug = params.slug || 'homepage'
const res = await fetch(
`${process.env.CMS_URL}/api/v1/content/${slug}?format=clean`
)
if (res.status === 404) {
throw error(404, 'Page not found')
}
return { page: await res.json() }
}
Fetch content by slug
The content delivery endpoint returns the full published content tree for a route.
curl:
curl http://localhost:8080/api/v1/content/homepage
Go SDK:
import modula "github.com/hegner123/modulacms/sdks/go"
client, err := modula.NewClient(modula.ClientConfig{
BaseURL: "http://localhost:8080",
APIKey: "YOUR_API_KEY",
})
if err != nil {
// handle error
}
raw, err := client.Content.GetPage(ctx, "homepage", "", "")
if err != nil {
if modula.IsNotFound(err) {
// 404 -- no published content at this slug
}
// handle error
}
TypeScript SDK:
import { ModulaClient, isNotFound } from '@modulacms/sdk'
const client = new ModulaClient({
baseUrl: 'https://cms.example.com',
apiKey: 'YOUR_API_KEY',
})
try {
const page = await client.getPage('homepage')
} catch (err) {
if (isNotFound(err)) {
// 404
}
}
Choose an output format
The format parameter controls the response shape. Use the format that best fits your frontend:
| Format | Description |
|---|---|
raw |
Native ModulaCMS tree structure (default) |
clean |
Simplified structure with flat field values |
contentful |
Contentful-compatible response format |
sanity |
Sanity.io-compatible response format |
strapi |
Strapi-compatible response format |
wordpress |
WordPress REST API-compatible response format |
curl "http://localhost:8080/api/v1/content/homepage?format=clean"
const page = await client.getPage('homepage', { format: 'clean' })
raw, err := client.Content.GetPage(ctx, "homepage", "clean", "")
Good to know: If you're migrating from another CMS, use the matching output format so your existing frontend code works with minimal changes.
Request localized content
Pass a locale code to get translated content. When a translation doesn't exist, ModulaCMS falls back along the locale's fallback chain (e.g., fr-CA falls back to fr, then to the default locale).
curl "http://localhost:8080/api/v1/content/homepage?locale=fr"
const page = await client.getPage('homepage', { format: 'clean', locale: 'fr' })
raw, err := client.Content.GetPage(ctx, "homepage", "clean", "fr")
Build navigation from routes
Fetch all active routes to build your site's navigation menu.
TypeScript SDK:
const routes = await client.listRoutes()
const nav = routes
.filter(r => r.status === 1)
.map(r => ({
label: r.title,
href: `/${r.slug}`,
}))
Go SDK:
routes, err := client.Routes.List(ctx)
if err != nil {
// handle error
}
type NavItem struct {
Label string
Href string
}
var nav []NavItem
for _, r := range routes {
if r.Status != 1 {
continue
}
nav = append(nav, NavItem{
Label: r.Title,
Href: "/" + string(r.Slug),
})
}
Good to know: Build nested navigation by using slug conventions. Slugs like
blog,blog/tutorials, andblog/newsimply a hierarchy you can parse on the client. See routing for a full hierarchical nav example.
Search and filter content
Use the query endpoint to list, search, and filter content by datatype. This is how you build blog indexes, product catalogs, and filtered content lists.
curl "http://localhost:8080/api/v1/query/blog-post?sort=-published_at&limit=10"
TypeScript SDK:
const result = await client.queryContent('blog-post', {
sort: '-published_at',
limit: 10,
})
for (const item of result.data) {
console.log(`${item.fields.title} (${item.published_at})`)
}
Go SDK:
result, err := client.Query.Query(ctx, "blog-post", &modula.QueryParams{
Sort: "-published_at",
Limit: 10,
})
if err != nil {
// handle error
}
for _, item := range result.Data {
fmt.Printf("%s (%s)\n", item.Fields["title"], item.PublishedAt)
}
Filter by field values:
const tutorials = await client.queryContent('blog-post', {
filters: { category: 'tutorials' },
sort: '-published_at',
limit: 20,
})
Pattern match with the like operator:
const results = await client.queryContent('blog-post', {
filters: { 'title[like]': '%tutorial%' },
})
Good to know: See querying content for the full filter syntax, all operators, and pagination patterns.
Handle errors
All SDKs return typed errors you can inspect for HTTP status codes.
TypeScript SDK:
import { isNotFound, isUnauthorized } from '@modulacms/sdk'
try {
const page = await client.getPage('nonexistent')
} catch (err) {
if (isNotFound(err)) {
// 404 -- render your 404 page
}
if (isUnauthorized(err)) {
// 401 -- invalid or missing API key
}
}
Go SDK:
raw, err := client.Content.GetPage(ctx, "nonexistent", "", "")
if err != nil {
if modula.IsNotFound(err) {
// 404
}
if modula.IsUnauthorized(err) {
// 401
}
var apiErr *modula.ApiError
if errors.As(err, &apiErr) {
fmt.Printf("HTTP %d: %s\n", apiErr.StatusCode, apiErr.Message)
}
}
Next steps
- Routing -- create routes and configure output formats
- Querying content -- filter, sort, and paginate content by datatype
- Media -- serve responsive images in your frontend