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
- Register a Custom Component
- Wire Up Authentication in the Admin
- Use Session State in Client Components
- Create a Page Programmatically
- Raw GraphQL from Any Runtime
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 mapimport { 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 environmentexport 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.
type TestimonialProps = { quote: string; author: string;};
export function Testimonial({ quote, author }: TestimonialProps) { return ( <figure> <blockquote>{quote}</blockquote> <figcaption>— {author}</figcaption> </figure> );}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):
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:
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
- Components & Schema - Field types, placement rules, and the export map
- Boilerplate Tour - How the generated project fits together
- API Reference - Every operation in detail
Last updated: 7/9/26, 6:42 AM