Client Quickstart
This guide walks a developer through connecting a Next.js hotel website to GammaCMS. By the end, you will have a working integration that pulls pages, rooms, menus, and other content from the GammaCMS public API.
The examples use Next.js 14+ with the App Router and TypeScript.
Prerequisites
Before you begin, make sure you have:
- A GammaCMS account with an organization created.
- A site created in the dashboard (
Dashboard -> Sites) with a domain configured (e.g.miragenegril.com). - An API key generated from
Settings -> API Keys. - Content added via the dashboard — pages, rooms, amenities, testimonials, menus, etc.
If you prefer a ready-to-run project, clone the Hotel Starter template and skip to configuration. It includes the HTTP utilities and rendering pipeline shown below.
1. Environment Variables
Create an .env.local in your Next.js project root:
GAMMACMS_API_URL=https://api.gammacms.com/api/public/v1
GAMMACMS_API_KEY=pk_your_api_key_here
GAMMACMS_SITE_DOMAIN=miragenegril.comSecurity note: These are server-side only variables. Never prefix them with
NEXT_PUBLIC_— the API key should not be exposed to the browser.
2. API Helper
Create a typed utility to call the GammaCMS API. This handles authentication, ISR caching, and error responses in one place.
export interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
results: T[];
}
const API_URL = process.env.GAMMACMS_API_URL!;
const API_KEY = process.env.GAMMACMS_API_KEY!;
const DOMAIN = process.env.GAMMACMS_SITE_DOMAIN!;
export async function fetchCMS<T>(path: string): Promise<T> {
const url = `${API_URL}/sites/${DOMAIN}${path}`;
const res = await fetch(url, {
headers: {
"X-API-Key": API_KEY,
Accept: "application/json",
},
next: { revalidate: 300 }, // ISR — refresh every 5 minutes
});
if (!res.ok) {
const body = await res.text();
throw new Error(`GammaCMS API error (${res.status}): ${body}`);
}
return res.json() as Promise<T>;
}Every call to fetchCMS automatically scopes requests to your site domain and authenticates with the API key.
3. Fetching Content
The public API is scoped to your site domain. All paths below are relative to /sites/{domain}.
| Content | Endpoint | Returns |
|---|---|---|
| Site info | / | Site metadata and settings |
| Pages list | /pages/ | Published pages |
| Page detail | /pages/<slug>/ | Single page with sections and content blocks |
| Rooms list | /rooms/ | Published rooms |
| Room detail | /rooms/<slug>/ | Room with amenities, images, and seasonal rates |
| Amenities | /amenities/ | Active amenities |
| Testimonials | /testimonials/ | Active testimonials |
| Navigation | /menus/<location>/ | Menu with nested items (header, footer, sidebar) |
| Media | /media/?type=image | Media files, filterable by type |
| Categories | /categories/ | Content categories |
4. Example: Hotel Rooms Page
A server component that fetches all published rooms and renders a listing.
import Image from "next/image";
import { fetchCMS, PaginatedResponse } from "@/lib/gammacms";
interface Room {
id: string;
name: string;
slug: string;
short_description: string;
featured_image_url: string;
max_guests: number;
bed_type: string;
is_featured: boolean;
}
export default async function RoomsPage() {
const { results: rooms } = await fetchCMS<PaginatedResponse<Room>>("/rooms/");
return (
<main>
<h1>Our Rooms</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{rooms.map((room) => (
<a key={room.id} href={`/rooms/${room.slug}`} className="group block">
<Image
src={room.featured_image_url}
alt={room.name}
width={800}
height={500}
className="h-auto w-full rounded-lg object-cover"
/>
<h2>{room.name}</h2>
<p>{room.short_description}</p>
<span>
{room.max_guests} guests · {room.bed_type}
</span>
</a>
))}
</div>
</main>
);
}5. Example: Room Detail with Rates
Fetch a single room by slug, including amenities and seasonal pricing.
import { fetchCMS } from "@/lib/gammacms";
interface RoomDetail {
name: string;
slug: string;
description: string;
images: { url: string; alt: string }[];
max_guests: number;
size: string;
bed_type: string;
amenities: { id: string; name: string; icon: string }[];
rates: {
season_name: string;
single_rate: string;
double_rate: string;
currency: string;
}[];
}
export default async function RoomDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const room = await fetchCMS<RoomDetail>(`/rooms/${slug}/`);
return (
<main>
<h1>{room.name}</h1>
<p>{room.description}</p>
<h2>Amenities</h2>
<ul>
{room.amenities.map((a) => (
<li key={a.id}>
{a.icon} {a.name}
</li>
))}
</ul>
<h2>Rates</h2>
<table>
<thead>
<tr>
<th>Season</th>
<th>Single</th>
<th>Double</th>
</tr>
</thead>
<tbody>
{room.rates.map((r) => (
<tr key={r.season_name}>
<td>{r.season_name}</td>
<td>${r.single_rate}</td>
<td>${r.double_rate}</td>
</tr>
))}
</tbody>
</table>
</main>
);
}All rates in GammaCMS are stored and displayed in USD ($). The
currencyfield on each rate confirms this.
6. Example: Navigation Menu
Fetch the header menu and render navigation links. Menu locations are header, footer, or sidebar.
import { fetchCMS } from "@/lib/gammacms";
interface MenuItem {
id: string;
title: string;
url: string;
target: string;
children: MenuItem[];
}
interface MenuResponse {
id: string;
name: string;
items: MenuItem[];
}
export async function Header() {
const menu = await fetchCMS<MenuResponse>("/menus/header/");
return (
<nav className="flex items-center gap-6">
{menu.items.map((item) => (
<a key={item.id} href={item.url} target={item.target}>
{item.title}
</a>
))}
</nav>
);
}For nested dropdown menus, recurse over item.children to render sub-items.
7. Caching & Revalidation
The fetchCMS helper defaults to ISR with a 300-second (5-minute) revalidation window. Content updates in the dashboard will appear on your site within 5 minutes.
Adjusting revalidation per endpoint:
// Faster revalidation for frequently updated content
const res = await fetch(url, {
headers: { "X-API-Key": API_KEY },
next: { revalidate: 60 }, // 1 minute
});On-demand revalidation:
Use Next.js revalidatePath() or revalidateTag() in a webhook handler to instantly refresh content when editors publish changes.
Fully dynamic pages:
For pages that should never be cached (e.g. search results), use cache: "no-store":
const res = await fetch(url, {
headers: { "X-API-Key": API_KEY },
cache: "no-store",
});8. Error Handling
Common API response codes and what they mean:
| Status | Meaning | Action |
|---|---|---|
200 | Success | Parse the response body |
401 | Invalid or missing API key | Check GAMMACMS_API_KEY in .env.local |
404 | Content not found | Verify the slug/path exists and content is published |
429 | Rate limited | Back off and retry after the Retry-After header |
Rate limit headers are included on every response:
X-RateLimit-Limit— maximum requests per windowX-RateLimit-Remaining— requests left in the current windowRetry-After— seconds to wait before retrying (only on429responses)
A production-ready error handler might look like:
export async function fetchCMS<T>(path: string): Promise<T> {
const url = `${API_URL}/sites/${DOMAIN}${path}`;
const res = await fetch(url, {
headers: {
"X-API-Key": API_KEY,
Accept: "application/json",
},
next: { revalidate: 300 },
});
if (!res.ok) {
const body = await res.text();
if (res.status === 401) {
throw new Error("GammaCMS: Invalid API key. Check GAMMACMS_API_KEY.");
}
if (res.status === 429) {
const retryAfter = res.headers.get("Retry-After") ?? "60";
throw new Error(`GammaCMS: Rate limited. Retry after ${retryAfter}s.`);
}
throw new Error(`GammaCMS API error (${res.status}): ${body}`);
}
return res.json() as Promise<T>;
}Next Steps
- SEO metadata — Use the page’s
meta_title,meta_description, andog_image_urlfields with Next.jsgenerateMetadata(). See the Headless Integration guide for a full example. - Section rendering — Build a block renderer to map GammaCMS page sections to React components. The Headless Integration guide covers this pattern.
- Preview mode — Use the admin API (
/api/v1/) with JWT authentication to preview draft content before publishing. - Multiple sites — If you manage multiple hotel properties, parameterize
GAMMACMS_SITE_DOMAINper route group or use runtime configuration.
Need help? Open a ticket or email support@gammacms.com.