Client Website Guides

Headless CMS & Client Website Integration Guides

Step-by-step developer tutorials for integrating the breeder.cloud REST API into headless Next.js, Astro, Remix, or static websites.

1. Connecting Next.js to the Headless CMS

Use standard fetch() to retrieve public organization templates, theme tokens, and dynamic pages.

Fetching CMS Pages & Template Tokens
// src/lib/breeder-api.ts
const BASE_URL = "https://breeder.cloud/api/v1";

export async function getOrganizationTemplate(orgSlug: string) {
  const res = await fetch(`${BASE_URL}/organizations/${orgSlug}/template`, {
    next: { revalidate: 300 },
  });
  if (!res.ok) throw new Error("Failed to load organization template");
  const json = await res.json();
  return json.data;
}

export async function getCmsPage(orgSlug: string, pageSlug: string) {
  const res = await fetch(`${BASE_URL}/organizations/${orgSlug}/pages/${pageSlug}`, {
    next: { revalidate: 60 },
  });
  if (!res.ok) return null;
  const json = await res.json();
  return json.data;
}

2. Submitting Inbound Inquiries & Contact Forms

The POST /api/v1/contact endpoint allows client websites to send leads directly to the breeder's inbox.

React Contact Form with Status Management
// React Contact Form Component
"use client";

import { useState } from "react";

export function ContactForm({ organizationId, animalId }: { organizationId: string; animalId?: string }) {
  const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
  const [errorMessage, setErrorMessage] = useState("");

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("submitting");
    setErrorMessage("");

    const formData = new FormData(e.currentTarget);
    const payload = {
      organizationId,
      animalId,
      name: formData.get("name") as string,
      email: formData.get("email") as string,
      phone: (formData.get("phone") as string) || undefined,
      subject: (formData.get("subject") as string) || "General Inquiry",
      message: formData.get("message") as string,
      language: "en",
    };

    try {
      const res = await fetch("https://breeder.cloud/api/v1/contact", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(payload),
      });

      const data = await res.json();
      if (!res.ok) {
        throw new Error(data.message || "Failed to submit contact inquiry");
      }

      setStatus("success");
    } catch (err: any) {
      setStatus("error");
      setErrorMessage(err.message || "An unexpected error occurred.");
    }
  }

  if (status === "success") {
    return (
      <div className="rounded-xl bg-emerald-50 p-6 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-200">
        <h4 className="font-bold">Thank you for your message!</h4>
        <p className="mt-1 text-sm">The breeder will get back to you shortly.</p>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      {status === "error" && (
        <div className="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-950 dark:text-red-300">
          {errorMessage}
        </div>
      )}
      <div>
        <label className="block text-xs font-semibold uppercase">Your Name</label>
        <input name="name" required className="mt-1 w-full rounded-lg border p-2 text-base" />
      </div>
      <div>
        <label className="block text-xs font-semibold uppercase">Email Address</label>
        <input name="email" type="email" required className="mt-1 w-full rounded-lg border p-2 text-base" />
      </div>
      <div>
        <label className="block text-xs font-semibold uppercase">Phone (Optional)</label>
        <input name="phone" type="tel" className="mt-1 w-full rounded-lg border p-2 text-base" />
      </div>
      <div>
        <label className="block text-xs font-semibold uppercase">Message</label>
        <textarea name="message" rows={4} required className="mt-1 w-full rounded-lg border p-2 text-base" />
      </div>
      <button
        type="submit"
        disabled={status === "submitting"}
        className="rounded-lg bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-500"
      >
        {status === "submitting" ? "Sending..." : "Send Message"}
      </button>
    </form>
  );
}

3. Navigation Menus & Dynamic Target Routes

Navigation menus (GET /api/v1/organizations/{id}/menus) support three link types:

PAGE

Links directly to a CMS page slug created inside the organization dashboard.

DYNAMIC

Maps to automated views: ANIMALS_OVERVIEW, ALL_ACTIVE_FEMALES, ALL_ACTIVE_MALES, LITTERS_OVERVIEW.

CUSTOM

External link or arbitrary custom path with optional target="_blank".

4. Real-Time Cache Invalidation Webhooks

Set your webhook URL via PUT /api/v1/organizations/{id}/webhook or POST /api/v1/webhook. breeder.cloud will immediately dispatch a POST request to your endpoint whenever animals, pages, sections, templates, or media change in the database.

Next.js On-Demand Cache Invalidation Route Handler
// app/api/revalidate/route.ts (Next.js App Router Invalidation Handler)
import { type NextRequest, NextResponse } from "next/server";
import { revalidateTag } from "next/cache";

export async function POST(req: NextRequest) {
  const secret = req.headers.get("x-webhook-secret");
  if (secret !== process.env.BREEDER_CLOUD_WEBHOOK_SECRET) {
    return NextResponse.json({ message: "Invalid secret" }, { status: 401 });
  }

  const payload = await req.json();
  const { event, tags, model, organizationId } = payload;

  if (event === "cache:invalidate" && Array.isArray(tags)) {
    // Invalidate cached Next.js fetch tags or CDN surrogate keys
    for (const tag of tags) {
      revalidateTag(tag);
    }
  }

  return NextResponse.json({ revalidated: true, tags, now: Date.now() });
}

5. Sticky Action Bar & Reusable Page Headers

The <StickyActionBar /> (or <StickyHeader />) component provides a standardized, sticky top action bar for editors and managers. It automatically sticks at top-14 (56px) to sit directly underneath the breeder.cloud dashboard navigation header without being covered on scroll.

OFFSET ALIGNMENT

Defaults to offset="navbar" (top-14), matching the 56px sticky dashboard navigation bar.

FULL CONTAINER BLEED

fullBleed=true stretches the action bar across the dashboard container padding (p-4 sm:p-6 lg:p-8) seamlessly.

STRUCTURED & COMPOUND

Use either high-level props (title, badges, meta, actions) or compound slots for complete layout freedom.

Prop-Based Sticky Action Bar Usage in an Editor Page
// Reusable Sticky Header / Action Bar
import { StickyActionBar } from "@/components/ui/sticky-action-bar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Save, Globe } from "lucide-react";

export function ExampleEditorPage() {
  return (
    <div className="space-y-6 pb-16">
      {/* Sticky Action Bar — define the primary action ONCE; the bar renders
          it in the title row below 1600px and in the actions row on wide
          screens automatically. */}
      <StickyActionBar
        backHref="/dashboard/organization"
        title="Page Editor"
        dirty={isDirty}
        badges={
          <Badge variant="success">
            <Globe className="h-2.5 w-2.5 mr-1" />
            Published
          </Badge>
        }
        meta={
          <div className="flex items-center gap-2 text-xs text-muted-foreground font-mono">
            <span>/about-us</span>
            <span>•</span>
            <span>3 sections</span>
          </div>
        }
        actions={
          <Button variant="outline" size="sm">Cancel</Button>
        }
        primaryAction={
          <Button size="sm" className="gap-1.5 font-semibold shadow-xs">
            <Save className="h-3.5 w-3.5" />
            Save Changes
          </Button>
        }
      />

      {/* Page Content */}
      <main>
        {/* Editor forms and panels */}
      </main>
    </div>
  );
}
Compound Subcomponents Pattern for Custom Header Layouts
// Flexible Compound Subcomponents Pattern
import {
  StickyActionBar,
  StickyActionBarLeft,
  StickyActionBarRight,
  StickyActionBarBackButton,
  StickyActionBarTitle,
  StickyActionBarMeta,
} from "@/components/ui/sticky-action-bar";
import { Button } from "@/components/ui/button";

export function CustomDashboardHeader() {
  return (
    <StickyActionBar>
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <StickyActionBarLeft>
          <StickyActionBarBackButton href="/dashboard" />
          <div>
            <StickyActionBarTitle>Breeding Program Manager</StickyActionBarTitle>
            <StickyActionBarMeta>Active Season 2026</StickyActionBarMeta>
          </div>
        </StickyActionBarLeft>

        <StickyActionBarRight>
          <Button variant="outline" size="sm">Export Report</Button>
          <Button size="sm">Create Match</Button>
        </StickyActionBarRight>
      </div>
    </StickyActionBar>
  );
}