Noma
Astro 5 · Content sites · 2026

CMS for Astro

Astro excels at mostly-static sites with islands of interactivity. Noma fits that model: fetch structured entries at build time or on the server, keep credentials out of the browser, and optionally mix in Astro server islands for deferred dynamic UI.

Fit

Why Astro and Noma work together

Astro’s default output is static HTML with optional hydration. Noma provides the structured data layer—collections, fields, locales, assets—while Astro handles templating and deployment. You can prerender thousands of entry pages at build time, or move individual routes to on-demand rendering when you add an adapter.

Astro 5 simplifies the old static vs hybrid matrix: with an adapter, routes that set prerender = false can render on the server while the rest of the site stays static. For adapters, on-demand rendering, and server islands, treat docs.astro.build as the source of truth—behavior and defaults change between majors, so verify against your installed version rather than only blog posts.

Historical context: Astro 5 release notes.

Start from the concise Astro guide in Documentation, then use this page as a production-oriented supplement.

Credentials

Projects, keys, and env vars

  • Create a Noma project and collections, then copy the project UUID and create an API key under the correct workspace (see Authentication).
  • In Astro, place secrets in .env / .env.production. Reference import.meta.env.NOMA_PROJECT_ID and import.meta.env.NOMA_API_KEY only in server contexts—frontmatter, endpoints, or server islands—not in client-side scripts.
  • Use PUBLIC_NOMA_PROJECT_ID when the browser must know the project id (for example project user auth flows). Never make the API key public.

The SDK always talks to https://app.nomacms.com/api; you do not configure a separate base URL for production SaaS.

Install

SDK helper module

npm install @nomacms/js-sdk
// src/lib/noma.ts
import { createClient } from "@nomacms/js-sdk";
 
export function getNomaServerClient() {
  const projectId = import.meta.env.NOMA_PROJECT_ID;
  const apiKey = import.meta.env.NOMA_API_KEY;
  if (!projectId || !apiKey) {
    throw new Error("Missing NOMA_PROJECT_ID or NOMA_API_KEY");
  }
  return createClient({ projectId, apiKey });
}
Static pages

Prerendered listings and detail routes

Fetch in component frontmatter. Large sites should paginate collection lists in getStaticPaths or split across multiple index pages.

---
// src/pages/posts/index.astro
import { getNomaServerClient } from "../../lib/noma";
 
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;
---
 
<ul>
  {posts.map((post) => (
    <li>
      <a href={`/posts/${post.uuid}/`}>{String(post.fields?.title ?? post.uuid)}</a>
    </li>
  ))}
</ul>
---
// src/pages/posts/[id].astro
import { getNomaServerClient } from "../../lib/noma";
 
export async function getStaticPaths() {
  const noma = getNomaServerClient();
  const result = await noma.content.list("posts", {
    state: "published",
    paginate: 500,
    page: 1,
  });
  const posts = "data" in result ? result.data : result;
  return posts.map((post) => ({ params: { id: post.uuid } }));
}
 
const { id } = Astro.params;
const noma = getNomaServerClient();
const post = await noma.content.get("posts", id!, { state: "published" });
---
 
<article>
  <h1>{String(post.fields?.title ?? "")}</h1>
</article>
SSR

Adapters and on-demand rendering

When you need fresh CMS reads on every request, add an official adapter (Node, Vercel, Netlify, etc.) and set export const prerender = false on those routes. The SDK should run in Node-compatible server contexts.

// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
 
export default defineConfig({
  output: "server",
  adapter: node({ mode: "standalone" }),
});
Server islands

Deferred dynamic fragments (Astro 5)

Server islands let you ship fast static shells while loading deferred server components after the first paint—useful for recommendations, carts, or account snippets without sacrificing CDN caching for the article body. Mark components with server:defer and provide fallback content; see Server islands in Astro docs.

Keep Noma API keys inside island server code, not in props that unnecessarily duplicate large payloads in island requests.

Endpoints

JSON routes and webhooks

Expose JSON for islands or third parties, or verify signed webhook deliveries from Noma to trigger rebuilds on your host.

// src/pages/api/posts.json.ts
import type { APIRoute } from "astro";
import { getNomaServerClient } from "../../lib/noma";
 
export const GET: APIRoute = async () => {
  const noma = getNomaServerClient();
  const data = await noma.content.list("posts", {
    state: "published",
    paginate: 20,
  });
  return new Response(JSON.stringify(data), {
    headers: { "Content-Type": "application/json" },
  });
};
Content Layer

Optional Astro Content Layer mapping

Astro’s Content Layer can load remote sources. You can map Noma entries into collection loaders and validate with Zod in src/content.config.ts if you want typed access alongside filesystem content. The Noma SDK remains the simplest path for teams that only need CMS-backed pages.

AI assistants

MCP and Astro skills

Install @nomacms/mcp-server for editor automation and add the noma-astro skill from nomacms-agent-skills so agents respect Astro + Noma patterns.

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.