Noma
Next.js · App Router · 2026

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.

Positioning

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.

Prerequisites

Dashboard, workspace, and API access

  • Create a project at app.nomacms.com and define at least one collection (for example posts with 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-id header selects which project to hit.
  • Grant abilities that match your integration: read for public sites, plus create/update if Route Handlers or automation write content.

HTTP details (headers, errors, rate limits) are summarized in Content API authentication.

Install

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-sdk

Add 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 });
}
Rendering

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;
  }
}
Static generation

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; // seconds
Next.js 15+ caching

SDK 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 = N on routes that should periodically refresh.
  • unstable_cache — wrap SDK calls in unstable_cache with tags and call revalidateTag from a Server Action or Route Handler when content changes.
  • On-demand — expose a secret-protected Route Handler and call revalidatePath when 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 });
}
API routes

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.

AI workflow

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.

Checklist

Before you ship

  • Confirm API keys are never referenced from Client Components or public env.
  • Use state: "published" for public marketing content.
  • Handle NotFoundError with notFound() 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.

Quickstart · SDK setup · Headless CMS overview

Now available

Start building with Noma

Create a free account, spin up a project, and ship structured content with our API, SDK, and AI tools.