Skip to main content
StormyCMS

Examples

Practical examples for building with StormyCMS. Most are drawn directly from the official Next.js boilerplate (stormy init), so you can cross-reference a working project.

Table of Contents

Render a CMS Page in Next.js

A catch-all route that renders any CMS page by slug, with its layout chain. This is the boilerplate's apps/site/src/app/(pages)/[...slug]/page.tsx:

import { redirect } from 'next/navigation';
import { mapPageData, type CMSPageData } from '@stormycms/core';
import { ComponentTree, LayoutTree, buildLayoutTree } from '@stormycms/react/cms';
import cmsExports from 'components'; // Your shared component export map
import { stormyCMSClient } from '~/stormy-cms-client';
export const dynamic = 'force-dynamic';
const getPage = async (slugParts?: string[]) => {
const slug = slugParts?.join('/') ?? '';
const page = await stormyCMSClient.getPageBySlug({ slug });
return page === null ? null : mapPageData(page);
};
export async function generateMetadata({ params }: { params: Promise<{ slug: string[] }> }) {
const page = await getPage((await params).slug);
if (page === null) return { title: 'Page Not Found' };
return { title: page.metadata.title, description: page.metadata.description };
}
export default async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
const page = await getPage((await params).slug);
if (page === null) redirect('/404');
const layout = buildLayoutTree(page.layouts, page.layoutId);
return (
<LayoutTree layout={layout} cmsExports={cmsExports}>
<ComponentTree components={page.components!} cmsExports={cmsExports} />
</LayoutTree>
);
}

With a shared client module (src/stormy-cms-client.ts):

import { StormyCMSClient } from '@stormycms/core';
// Reads STORMY_CMS_CLIENT_ID and STORMY_CMS_CLIENT_SECRET from the environment
export const stormyCMSClient = new StormyCMSClient();

mapPageData converts the raw GraphQL shape (props/attrs as name-value pairs) into ergonomic objects (props as a plain record), which is what ComponentTree expects.

Register a Custom Component

Write a normal React component, then register it in your shared export map with withCMS. Both the public site and the admin editor import this map.

packages/components/src/testimonial.tsx
type TestimonialProps = {
quote: string;
author: string;
};
export function Testimonial({ quote, author }: TestimonialProps) {
return (
<figure>
<blockquote>{quote}</blockquote>
<figcaption>— {author}</figcaption>
</figure>
);
}
packages/components/index.ts
import { withCMS } from '@stormycms/react/cms';
import { Testimonial } from './src/testimonial';
export default {
// ...existing components
Testimonial: withCMS(
{
export: 'Testimonial',
label: 'Testimonial',
fields: [
{ label: 'Quote', prop: 'quote', field: 'textarea', required: true, defaultValue: '' },
{ label: 'Author', prop: 'author', field: 'input', required: true, defaultValue: '' },
],
noChildren: true,
},
Testimonial,
),
};

The admin editor immediately shows "Testimonial" as an insertable component with a quote textarea and an author input — no external schema portal involved. See Components & Schema for all field types and placement rules.

Wire Up Authentication in the Admin

One route handler gives you the complete OAuth flow (sign-in, callback, session, sign-out):

apps/admin/src/app/api/auth/stormy/route.ts
import { createNextAuthHandlers } from '@stormycms/next';
const { GET, POST } = createNextAuthHandlers();
export { GET, POST };

Then guard your admin pages with the server provider, which loads the session, current site, and site list before rendering:

apps/admin/src/app/layout.tsx
import { StormyServerProvider } from '@stormycms/next';
import { stormyCMSClient } from '~/stormy-cms-client';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<StormyServerProvider client={stormyCMSClient} loginUrl="/login">
{children}
</StormyServerProvider>
</body>
</html>
);
}

Unauthenticated users are redirected to /login, where you can trigger sign-in with a link to /api/auth/stormy?action=signin&provider=github (or provider=google).

Use Session State in Client Components

Inside the provider, any client component can read the session:

'use client';
import { useStormy } from '@stormycms/react/stormy';
export function AccountBadge() {
const { user, site, isLoading, isAuthenticated } = useStormy();
if (isLoading) return <span>…</span>;
if (!isAuthenticated) return <a href="/login">Sign in</a>;
return (
<span>
{user!.userName} — editing {site?.name ?? 'no site'}
</span>
);
}

Create a Page Programmatically

Editor mutations require a JWT derived from the signed-in user's session. In a Next.js server action or route handler:

import { cookies } from 'next/headers';
import { StormyCMSClient } from '@stormycms/core';
export async function createAboutPage() {
const client = new StormyCMSClient();
const cookieHeader = (await cookies()).toString();
const jwt = await client.getJWTToken(cookieHeader);
if (!jwt) throw new Error('Not signed in');
const now = new Date().toISOString();
return client.createPage(jwt, {
slug: 'about',
metadata: { title: 'About', description: 'About our company', keywords: [] },
components: [
{ name: 'Heading', props: [{ name: 'text', value: 'About' }] },
{ name: 'Text', props: [{ name: 'content', value: 'Hello world.' }] },
],
layoutId: 'YOUR_LAYOUT_ID',
createdAt: now,
updatedAt: now,
});
}

Raw GraphQL from Any Runtime

StormyCMS works with any GraphQL client. With plain fetch:

const response = await fetch('https://api.stormycms.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-client-id': process.env.STORMY_CMS_CLIENT_ID,
'x-client-secret': process.env.STORMY_CMS_CLIENT_SECRET,
},
body: JSON.stringify({
query: `
query GetPageBySlug($slug: String!) {
pageBySlug(slug: $slug) {
id
slug
metadata { title description }
components {
id
name
props { name value }
childComponents { id name props { name value } }
}
}
}
`,
variables: { slug: 'home' },
}),
});
const { data, errors } = await response.json();
if (errors?.length) throw new Error(errors[0].message);
console.log(data.pageBySlug);

This makes StormyCMS usable from static site generators, mobile backends, or any non-React stack — you just won't get the shared-component editor integration that the React packages provide.

Next Steps

Last updated: 7/9/26, 6:42 AM

StormyCMSThe headless CMS where you own the management panel
Community
StormyCMS