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.
// 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 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:
Links directly to a CMS page slug created inside the organization dashboard.
Maps to automated views: ANIMALS_OVERVIEW, ALL_ACTIVE_FEMALES, ALL_ACTIVE_MALES, LITTERS_OVERVIEW.
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.
// 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.
Defaults to offset="navbar" (top-14), matching the 56px sticky dashboard navigation bar.
fullBleed=true stretches the action bar across the dashboard container padding (p-4 sm:p-6 lg:p-8) seamlessly.
Use either high-level props (title, badges, meta, actions) or compound slots for complete layout freedom.
// 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>
);
}// 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>
);
}