CMS for Gatsby
Gatsby excels at build-time data fusion. Noma is a natural headless source: fetch published entries in gatsby-node.js with the JavaScript SDK, pass them through pageContext, and deploy static HTML—then let webhooks kick off rebuilds when content changes.
Gatsby + headless Noma
Unlike a gatsby-source-* plugin, a thin createPages integration keeps full control over queries, locales, and publish state (state: "published"). You trade GraphQL introspection of CMS fields for straightforward TypeScript-friendly objects from the SDK.
If you later want GraphQL, add a custom source plugin or use sourceNodes in gatsby-node.js to push Noma entries into Gatsby’s data layer—start simple, evolve when multiple consumers need the same normalized nodes.
SDK and environment
npm install @nomacms/js-sdk gatsbyAdd secrets to .env.development / production CI— no GATSBY_ prefix on the API key:
NOMA_PROJECT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
NOMA_API_KEY=noma_...Load env in gatsby-config via require("dotenv").config() or your host’s injected variables so process.env is populated during gatsby build.
createPages pattern
Fetch once per build, normalize the list vs paginated response shape, then call createPage for the index and each detail route:
// gatsby-node.js
const path = require("path");
const { createClient } = require("@nomacms/js-sdk");
exports.createPages = async ({ actions }) => {
const { createPage } = actions;
const noma = createClient({
projectId: process.env.NOMA_PROJECT_ID,
apiKey: process.env.NOMA_API_KEY,
});
const result = await noma.content.list("posts", {
state: "published",
paginate: 100,
page: 1,
sort: "created_at:desc",
});
const posts = "data" in result ? result.data : result;
createPage({
path: "/posts",
component: path.resolve("src/templates/posts.tsx"),
context: { posts },
});
for (const post of posts) {
createPage({
path: `/posts/${post.uuid}`,
component: path.resolve("src/templates/post.tsx"),
context: { post },
});
}
};// src/templates/posts.tsx
import * as React from "react";
import { Link } from "gatsby";
import type { PageProps } from "gatsby";
type CmsEntry = { uuid: string; fields: Record<string, unknown> };
const PostsTemplate: React.FC<PageProps<object, { posts: CmsEntry[] }>> = ({
pageContext,
}) => (
<ul>
{pageContext.posts.map((post) => (
<li key={post.uuid}>
<Link to={`/posts/${post.uuid}`}>
{String(post.fields?.title ?? post.uuid)}
</Link>
</li>
))}
</ul>
);
export default PostsTemplate;// src/templates/post.tsx
import * as React from "react";
import type { PageProps } from "gatsby";
type CmsEntry = {
uuid: string;
fields: Record<string, unknown>;
};
const PostTemplate: React.FC<PageProps<object, { post: CmsEntry }>> = ({
pageContext,
}) => {
const { post } = pageContext;
return (
<article>
<h1>{String(post.fields?.title ?? "")}</h1>
</article>
);
};
export default PostTemplate;For large collections, paginate in a loop in gatsby-node.js or fetch by updated cursor if you add indexing—respect Noma rate limits.
Optional preview builds
Use a separate CI job or branch build with a read-capable key and state: "draft" in SDK calls, protected by basic auth at the CDN. Never expose draft content on the public production domain.
Rebuild on publish
// Netlify / similar: call your build hook from a Noma webhook.
// Verify a shared secret in a small serverless function if you expose a public URL.Point Noma webhooks at your provider’s build hook or an authenticated endpoint that triggers gatsby build. Debounce bursts if editors publish many entries in sequence.
MCP and skills
Editors and developers can use @nomacms/mcp-server from Cursor or Claude Code. The skills repo is strongest for Next/Nuxt/Astro; pair it with this page for Gatsby-specific createPages wiring—see agent skills.