Noma

Blog

Headless CMS for Next.js: What Developers Actually Need

March 27, 2026

Teams often pick a CMS for Next.js based on demos, then hit problems during implementation.

The better approach is evaluating frontend delivery constraints first: stable field shapes, locale and draft/published semantics, and a client that maps cleanly to your data layer.

What matters in a Next.js CMS setup

For production apps, a CMS should support:

  • Structured content modeling
  • Stable API response shapes
  • Locale-aware delivery
  • Draft and publish workflow
  • Predictable caching and revalidation patterns

If one of these is weak, frontend complexity rises quickly.

Schema first, components second

Before building UI blocks, model reusable content entities:

  • Page
  • Section
  • CTA
  • SEO metadata
  • Locale variant (linked translations, not orphan copies)

This helps you avoid ad hoc JSON fields and brittle rendering logic.

API patterns that keep frontend code clean

Use APIs that let you:

  • Fetch by slug and locale
  • Request specific fields (or exclude heavy fields on lists)
  • Resolve references in one request
  • Filter by status (draft vs published)

If every page needs custom transformation code, the model is not stable enough. Noma’s REST API supports these dimensions directly—see How to Design Stable Content APIs for Frontend Teams.

Using the Noma JavaScript SDK in Next.js

Noma ships an official TypeScript/JavaScript SDK for the project REST API. Install it from npm:

npm install @elmapicms/js-sdk

The package exposes createClient, which sends the required project-id header and Authorization: Bearer for your project API token. Content methods mirror the HTTP API: client.content.list, client.content.get, create, update, and bulk helpers.

Environment variables (example):

NOMA_API_BASE_URL=https://your-instance.com/api NOMA_PROJECT_ID=550e8400-e29b-41d4-a716-446655440000 NOMA_PROJECT_API_TOKEN=your-project-api-token

Shared server client (App Router):

// lib/noma-server.ts import { createClient } from "@elmapicms/js-sdk"; export function getNomaServerClient() { const baseUrl = process.env.NOMA_API_BASE_URL; const projectId = process.env.NOMA_PROJECT_ID; const projectApiToken = process.env.NOMA_PROJECT_API_TOKEN; if (!baseUrl || !projectId || !projectApiToken) { throw new Error("Missing NOMA_API_BASE_URL, NOMA_PROJECT_ID, or NOMA_PROJECT_API_TOKEN"); } return createClient({ baseUrl, projectId, projectApiToken, }); }

List published posts in a Server Component:

// app/blog/page.tsx import { getNomaServerClient } from "@/lib/noma-server"; export const revalidate = 60; export default async function BlogPage() { const client = getNomaServerClient(); const result = await client.content.list("posts", { state: "published", locale: "en", sort: "published_at:desc", paginate: 12, }); // `result` matches the API (e.g. paginated collection resource); map `data` / items to your UI. return ( <main> {/* render entries from result */} </main> ); }

Single entry by UUID with optional translation lookup (same translation_group_id, matching state):

const client = getNomaServerClient(); const entry = await client.content.get("posts", postUuid, { state: "published", locale: "en", translation_locale: "fr", // optional: resolve linked FR entry exclude: ["body"], // optional: trim payload for previews });

Smaller list payloads by excluding large fields:

await client.content.list("posts", { state: "published", locale: "en", exclude: ["body", "content"], paginate: 20, });

Error handling — the SDK throws typed errors (for example NotFoundError, ValidationError):

import { notFound } from "next/navigation"; import { NotFoundError } from "@elmapicms/js-sdk"; try { await client.content.get("posts", uuid, { state: "published" }); } catch (e) { if (e instanceof NotFoundError) { notFound(); } throw e; }

The SDK also supports project user auth (sign-in, refresh, API keys) when you need end-user auth against Noma—see the package README for signInWithPassword and tokenStorage patterns for client components.

Revalidation: use export const revalidate = N on route segments, revalidatePath / revalidateTag from webhooks after publish, or fetch with next: { revalidate } if you mix raw fetch with the SDK. The SDK uses HTTP requests; time-based ISR is controlled by Next.js on the route that calls it.

Publishing workflow for product teams

A good headless CMS for Next.js should support editor workflows without breaking dev velocity.

At minimum:

  1. Draft content
  2. Review changes
  3. Publish with clear versioning
  4. Trigger frontend refresh (webhook → revalidate)

When this loop is smooth, developers ship faster.

How Noma fits this workflow

Noma combines structured modeling, API-first delivery, and AI-assisted generation/translation in one workflow. The SDK exposes the same primitives your app needs in production: state, locale, where, sort, paginate, and translation_locale on reads.

See also:

SDK reference

  • Package: @elmapicms/js-sdk (npm)
  • Source: nomacms-js-sdk in the Noma Platform tooling; same API surface as Noma REST Content endpoints

Final checklist

Before committing to any CMS for Next.js, validate:

  • Content model clarity
  • API predictability (and SDK coverage if you use Noma)
  • Localization support
  • Publish and rollback process
  • Performance and cache strategy

Those five factors drive most long-term outcomes.