Client Quickstart

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:

  1. A GammaCMS account with an organization created.
  2. A site created in the dashboard (Dashboard -> Sites) with a domain configured (e.g. miragenegril.com).
  3. An API key generated from Settings -> API Keys.
  4. 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:

.env.local
GAMMACMS_API_URL=https://api.gammacms.com/api/public/v1
GAMMACMS_API_KEY=pk_your_api_key_here
GAMMACMS_SITE_DOMAIN=miragenegril.com

Security 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.

lib/gammacms.ts
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}.

ContentEndpointReturns
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=imageMedia 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.

app/rooms/page.tsx
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 &middot; {room.bed_type}
            </span>
          </a>
        ))}
      </div>
    </main>
  );
}

5. Example: Room Detail with Rates

Fetch a single room by slug, including amenities and seasonal pricing.

app/rooms/[slug]/page.tsx
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 currency field on each rate confirms this.

6. Example: Navigation Menu

Fetch the header menu and render navigation links. Menu locations are header, footer, or sidebar.

components/header.tsx
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:

StatusMeaningAction
200SuccessParse the response body
401Invalid or missing API keyCheck GAMMACMS_API_KEY in .env.local
404Content not foundVerify the slug/path exists and content is published
429Rate limitedBack off and retry after the Retry-After header

Rate limit headers are included on every response:

  • X-RateLimit-Limit — maximum requests per window
  • X-RateLimit-Remaining — requests left in the current window
  • Retry-After — seconds to wait before retrying (only on 429 responses)

A production-ready error handler might look like:

lib/gammacms.ts
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, and og_image_url fields with Next.js generateMetadata(). 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_DOMAIN per route group or use runtime configuration.

Need help? Open a ticket or email support@gammacms.com.