CMS for Next.js
Noma is an API-first headless CMS: model content in the dashboard, publish entries, and render them in Next.js with the official JavaScript SDK without exposing secrets to the browser. This guide mirrors our product docs and adds deployment, caching, and SEO-oriented detail.
Why teams pair Noma with Next.js
Next.js is the default React framework for content-heavy sites. Noma stays in a dedicated content layer: structured collections, localization, assets, webhooks, and AI workflows in the dashboard; your Next app consumes published snapshots through the Content API. That separation keeps editorial tools out of your git history and lets you ship UI changes independently of CMS releases.
For a shorter tutorial, start with the official Next.js framework guide in Documentation, then use this page as a checklist for production.
Dashboard, workspace, and API access
- Create a project at app.nomacms.com and define at least one collection (for example
postswith title, slug, and body fields). - Copy the Project ID from the project home or project settings.
- Under User settings → API keys, create a personal access token while the correct workspace is selected. The token is scoped to that workspace; the
project-idheader selects which project to hit. - Grant abilities that match your integration:
readfor public sites, pluscreate/updateif Route Handlers or automation write content.
HTTP details (headers, errors, rate limits) are summarized in Content API authentication.
JavaScript SDK in a Next.js repo
Install the official client. It targets Noma SaaS at https://app.nomacms.com/api and sends Authorization: Bearer … and project-id for you.
npm install @nomacms/js-sdkAdd server-only environment variables in .env.local (never prefix the API key with NEXT_PUBLIC_):
NOMA_PROJECT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
NOMA_API_KEY=noma_...Centralize construction so every server entry point behaves the same:
// lib/noma.ts
import { createClient } from "@nomacms/js-sdk";
export function getNomaServerClient() {
const projectId = process.env.NOMA_PROJECT_ID;
const apiKey = process.env.NOMA_API_KEY;
if (!projectId || !apiKey) {
throw new Error("Missing NOMA_PROJECT_ID or NOMA_API_KEY");
}
return createClient({ projectId, apiKey });
}Server Components for reads
In the App Router, default to async Server Components for listing and detail pages. The SDK runs on the server per request (or per your caching configuration below).
// app/posts/page.tsx
import { getNomaServerClient } from "@/lib/noma";
export default async function PostsPage() {
const noma = getNomaServerClient();
const result = await noma.content.list("posts", {
state: "published",
paginate: 24,
sort: "created_at:desc",
});
const posts = "data" in result ? result.data : result;
return (
<ul>
{posts.map((post) => (
<li key={post.uuid}>
<a href={`/posts/${post.uuid}`}>
{String(post.fields?.title ?? post.uuid)}
</a>
</li>
))}
</ul>
);
}List response shape: with paginate, responses are typically { data, meta, links }. With limit only, you may receive a bare array. Singleton collections return a single object from list—see Content SDK reference.
// app/posts/[id]/page.tsx
import { notFound } from "next/navigation";
import { NotFoundError } from "@nomacms/js-sdk";
import { getNomaServerClient } from "@/lib/noma";
type Props = { params: Promise<{ id: string }> };
export default async function PostPage({ params }: Props) {
const { id } = await params;
const noma = getNomaServerClient();
try {
const post = await noma.content.get("posts", id, { state: "published" });
return (
<article>
<h1>{String(post.fields?.title ?? "")}</h1>
</article>
);
} catch (error) {
if (error instanceof NotFoundError) notFound();
throw error;
}
}generateStaticParams and ISR
For marketing pages, you often want HTML generated at build time or periodically refreshed. Use generateStaticParams to enumerate entry UUIDs (or slugs if you store them in fields).
import { getNomaServerClient } from "@/lib/noma";
export async function generateStaticParams() {
const noma = getNomaServerClient();
const res = await noma.content.list("posts", {
state: "published",
paginate: 100,
page: 1,
});
const entries = Array.isArray(res) ? res : res.data;
return entries.map((post) => ({ id: post.uuid }));
}Add a segment-level revalidate interval so Incremental Static Regeneration refreshes on a timer:
export const revalidate = 60; // secondsSDK calls, fetch, and revalidateTag
In current Next.js releases, the framework's Data Cache is centered on fetch semantics. The Noma SDK uses its own HTTP stack, so it does not automatically participate in fetch cache tags. Practical options:
- Segment revalidation — set
export const revalidate = Non routes that should periodically refresh. - unstable_cache — wrap SDK calls in
unstable_cachewithtagsand callrevalidateTagfrom a Server Action or Route Handler when content changes. - On-demand — expose a secret-protected Route Handler and call
revalidatePathwhen Noma webhooks fire.
Newer Next.js releases also introduce Cache Components (cacheComponents in config) with the "use cache" directive, plus cacheTag / cacheLife for fine-grained invalidation alongside revalidateTag and updateTag. If you adopt that model, prefer it over ad-hoc unstable_cache for new code; the SDK still does not participate in fetch cache tags, so you wrap reads the same way but align tags with the framework's current APIs. See Revalidating and caching in the App Router for the latest guidance.
Example webhook-style handler (adapt payload parsing to your signed webhook body):
import { revalidatePath } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const secret = request.headers.get("x-revalidate-secret");
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const body = await request.json().catch(() => ({}));
if (body?.collection === "posts" && body?.uuid) {
revalidatePath(`/posts/${body.uuid}`);
revalidatePath("/posts");
}
return NextResponse.json({ revalidated: true });
}Route Handlers and Server Actions
Use Route Handlers when the browser needs JSON, you proxy uploads, or you verify webhooks. Prefer Server Components when you only render HTML—fewer hops, simpler caching story.
For end-user authentication against Noma Project Auth, keep OAuth token exchange in Route Handlers or Server Actions; never log raw provider tokens on the client.
MCP server and Next.js agent skills
To let Cursor, Claude Code, or other MCP clients manage schema and content from chat, run the official @nomacms/mcp-server package. Install Noma agent skills such as noma-nextjs so assistants follow Next.js + SDK conventions automatically. Product overview: MCP Server, Skills.
Before you ship
- Confirm API keys are never referenced from Client Components or public env.
- Use
state: "published"for public marketing content. - Handle
NotFoundErrorwithnotFound()for clean 404s. - Plan revalidation (time-based, tag-based, or webhook-driven) for content freshness.
- Respect API rate limits; cache lists at the edge or server where appropriate.