Headless Integration Guide
This guide walks through consuming the GammaCMS public API from a dedicated hotel website. You can adopt the snippets below in a Next.js, Remix, Astro, or Nuxt project. The examples assume Next.js 14+ with the App Router because it aligns with the rest of GammaCMS.
Prerequisites
- An active organization in GammaCMS (e.g. Hampton Bay Retreat).
- At least one published site (
Dashboard → Sites) with pages seeded by the hotel template. - A public API key created under
Dashboard → Settings → API Keys. - The organization UUID (visible in the same settings screen or via browser dev tools on the dashboard).
💡 Prefer a ready-to-run project? Clone the new Hotel Starter template and skip directly to configuration. It includes the HTTP utilities, section renderer, and ISR wiring shown below.
Create an .env.local in the hotel front end with:
GAMMACMS_API_URL=https://api.gammacms.com/api/public/v1
GAMMACMS_API_KEY=pk_live_XXXXXXXXXXXXXXXX
GAMMACMS_ORGANIZATION_ID=ec9a9dcb-24d7-4074-9f5c-9c2fcee5c462
GAMMACMS_SITE_DOMAIN=hamptonbay.exampleHTTP helper
const baseUrl = process.env.GAMMACMS_API_URL!;
const apiKey = process.env.GAMMACMS_API_KEY!;
const organizationId = process.env.GAMMACMS_ORGANIZATION_ID!;
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${baseUrl}${path}`, {
headers: {
"X-API-Key": apiKey,
"X-Organization-ID": organizationId,
Accept: "application/json",
...init.headers,
},
next: { revalidate: 300 }, // ISR every 5 minutes
});
if (!response.ok) {
const message = await response.text();
throw new Error(`GammaCMS request failed (${response.status}): ${message}`);
}
return response.json() as Promise<T>;
}
export async function getSite(domain = process.env.GAMMACMS_SITE_DOMAIN!) {
return request(`/sites/${domain}/`);
}
export async function getPages(domain = process.env.GAMMACMS_SITE_DOMAIN!) {
return request(`/sites/${domain}/pages/`);
}
export async function getPage(
slug: string,
domain = process.env.GAMMACMS_SITE_DOMAIN!
) {
return request(`/sites/${domain}/pages/${slug}/`);
}
export async function getMenus(domain = process.env.GAMMACMS_SITE_DOMAIN!) {
return request(`/sites/${domain}/menus/`);
}
export async function getMedia(
domain = process.env.GAMMACMS_SITE_DOMAIN!,
type?: string
) {
const query = type ? `?type=${encodeURIComponent(type)}` : "";
return request(`/sites/${domain}/media/${query}`);
}Static site generation (Next.js App Router)
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getPage, getPages, getSite } from "@/lib/gammacms";
import { renderSections } from "@/lib/render-sections"; // custom renderer described below
export async function generateStaticParams() {
const pages = await getPages();
return pages.map(({ slug }: { slug: string }) => ({ slug }));
}
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const page = await getPage(params.slug).catch(() => null);
if (!page) return {};
return {
title: page.meta_title ?? page.title,
description: page.meta_description ?? page.excerpt,
openGraph: {
images: page.og_image_url ? [{ url: page.og_image_url }] : undefined,
type: page.og_type ?? "website",
},
} satisfies Metadata;
}
export default async function Page({ params }: { params: { slug: string } }) {
const page = await getPage(params.slug).catch(() => null);
if (!page) {
notFound();
}
return <main>{renderSections(page.sections)}</main>;
}
export async function generateStaticMetadata() {
const site = await getSite();
return {
title: site.meta_title ?? site.name,
description: site.meta_description,
icons: {
icon: site.favicon_url ?? "/favicon.ico",
},
} satisfies Metadata;
}Rendering sections
GammaCMS pages can be presented as sections (the default for hotel templates) or pure Lexical rich text. A simple renderer might look like:
import { LexicalRichText } from "@/components/lexical-rich-text"; // wrap lexical viewer
interface Section {
id: string;
type: string;
layout: string;
background?: string;
padding?: string;
blocks: Array<{
id: string;
type: string;
content: unknown;
settings?: Record<string, unknown>;
}>;
}
export function renderSections(sections: Section[]) {
return sections.map((section) => {
const className = [
"section",
section.layout === "container" ? "mx-auto max-w-6xl" : "w-full",
section.padding === "large"
? "py-16"
: section.padding === "medium"
? "py-12"
: "py-8",
section.background === "muted"
? "bg-muted"
: section.background === "secondary"
? "bg-secondary"
: "bg-background",
].join(" ");
return (
<section key={section.id} className={className}>
<div className="space-y-6">
{section.blocks.map((block) => {
switch (block.type) {
case "text":
case "heading":
return (
<LexicalRichText key={block.id} content={block.content} />
);
case "button":
return (
<a
key={block.id}
href={String(block.settings?.url ?? "#")}
className="inline-flex items-center rounded bg-primary px-6 py-3 text-primary-foreground"
>
{String(block.content)}
</a>
);
case "image":
return (
<img
key={block.id}
src={String(
(block.content as { src: string })?.src ??
block.settings?.src ??
""
)}
alt={String(block.settings?.alt ?? "")}
className="w-full rounded-lg object-cover"
/>
);
default:
return null;
}
})}
</div>
</section>
);
});
}You can progressively enhance the renderer to map additional block types (galleries, testimonials, etc.).
Incremental static regeneration (ISR)
Because GammaCMS content changes frequently, we recommend revalidating pages using ISR:
export const revalidate = 300; // page will be refreshed every 5 minutesClient navigation & menus
Use the menus endpoint to power navigation components:
import { getMenus } from "@/lib/gammacms";
export async function MainNav() {
const menus = await getMenus();
const headerMenu = menus.find((menu: any) => menu.location === "header");
if (!headerMenu) return null;
return (
<nav className="flex items-center gap-6">
{headerMenu.items.map((item: any) => (
<a
key={item.id}
className="text-sm font-medium"
href={item.url ?? (item.page ? `/${item.page.slug}` : "#")}
>
{item.label}
</a>
))}
</nav>
);
}Media helper
Media entries already contain absolute URLs, alt text, and image dimensions. You can pipe them straight into <Image /> components from Next or Compose a CDN transformation.
import Image from "next/image";
export function HeroImage({ media }: { media: any }) {
return (
<Image
src={media.file_url}
alt={media.alt_text ?? media.title ?? ""}
width={media.width ?? 1600}
height={media.height ?? 900}
className="h-auto w-full rounded-lg object-cover"
priority
/>
);
}Starter template
A ready-to-clone hotel starter lives in the repository under templates/hotel-starter (see roadmap). It implements the same patterns described above:
- Next.js App Router with ISR.
- Tailwind CSS themed to match Hotel Classic.
- Headless navigation, booking CTA, rooms listing, dining highlights.
.env.examplewith the required GammaCMS variables.
Clone the template, set the environment variables, and deploy to Vercel/Netlify.
Tips
- Cache control: adjust the
next: { revalidate }values per endpoint. Pages may need shorter revalidation than media assets. - Preview draft content: use the admin API (
/api/v1/) with JWT tokens to build preview modes. - Multiple hotels: if you run multiple hotel sites from a single front end, parameterise the
GAMMACMS_SITE_DOMAINand organization headers at runtime (e.g. via route groups or dynamic config). - Error budgets: monitor
429responses and increase the per-key rate limit in the dashboard if you see sustained bursts at peak booking seasons.
Need more help? Open a ticket or email support@gammacms.com with details about your integration.