@akhdigital/reap-cms-sdk
v1.0.0
Published
TypeScript SDK for Reap CMS headless API
Maintainers
Readme
@reap/cms-sdk
TypeScript SDK for Reap CMS headless API with React hooks support.
Installation
npm install @reap/cms-sdkQuick Start
Basic Usage (Astro, Node, any JS environment)
import { ReapClient } from '@reap/cms-sdk';
const cms = new ReapClient({
baseUrl: 'https://your-project.supabase.co/functions/v1',
apiKey: 'your-api-key',
});
// Get all content
const content = await cms.getContent();
// Get content for a specific page
const homeContent = await cms.getContentByPage('home');
// Get typed content instance
interface HeroData {
title: string;
subtitle?: string;
background_image?: string;
}
const hero = await cms.getContentInstance<HeroData>('sections', 'hero');
console.log(hero.data.title);React Usage
import { ReapClient, ReapProvider, useContent, useGlobals } from '@reap/cms-sdk/react';
// Create client
const client = new ReapClient({
baseUrl: import.meta.env.VITE_REAP_API_URL,
apiKey: import.meta.env.VITE_REAP_API_KEY,
});
// Wrap your app
function App() {
return (
<ReapProvider client={client}>
<HomePage />
</ReapProvider>
);
}
// Use hooks in components
function HomePage() {
const { data: globals, isLoading: globalsLoading } = useGlobals();
const { data: content, isLoading: contentLoading } = useContentByPage('home');
if (globalsLoading || contentLoading) {
return <div>Loading...</div>;
}
return (
<div>
<header>
<img src={globals?.logo_url} alt={globals?.name} />
</header>
{content?.map(section => (
<Section key={section.id} data={section} />
))}
</div>
);
}API Reference
ReapClient
Constructor
new ReapClient({
baseUrl: string; // Supabase functions URL
apiKey: string; // Your API key
timeout?: number; // Request timeout in ms (default: 10000)
})Content Methods
| Method | Description |
|--------|-------------|
| getContent<T>() | Get all published content |
| getContentByPage<T>(pageSlug) | Get content for a specific page |
| getContentByType<T>(typeSlug) | Get content by type |
| getContentInstance<T>(typeSlug, instanceSlug) | Get single content instance |
| getContentBulk(endpoints) | Fetch multiple content items in one request |
Forms Methods
| Method | Description |
|--------|-------------|
| getForms() | Get all forms with fields and reCAPTCHA key |
| submitForm(formId, data, recaptchaToken) | Submit a form |
Other Methods
| Method | Description |
|--------|-------------|
| getGlobals() | Get site configuration (branding, contact, meta tags) |
| getSpecials() | Get active specials/promotions |
React Hooks
All hooks return a consistent state object:
{
data: T | null;
error: Error | null;
isLoading: boolean;
isError: boolean;
isSuccess: boolean;
isFetching: boolean;
refetch: () => Promise<void>;
}Content Hooks
// All content
const { data } = useContent<T>();
// Content by page
const { data } = useContentByPage<T>('home');
// Content by type
const { data } = useContentByType<T>('floorplans');
// Single content instance
const { data } = useContentInstance<HeroData>('sections', 'hero');
// Bulk fetch
const { data } = useContentBulk(['sections/hero', 'sections/amenities']);Forms Hooks
// All forms with submit function
const { data, submitForm, isSubmitting, submitError } = useForms();
// Specific form by ID or name
const { form, submitForm, isSubmitting } = useForm('contact');Globals Hooks
// Full globals
const { data } = useGlobals();
// Specific meta tag
const { value } = useGlobalTag('description');
// Brand colors
const { colors } = useBrandColors();
// colors.primary, colors.secondary, colors.tertiary
// Social media links
const { links } = useSocialMedia();
// links.facebook, links.instagram, etc.Specials Hooks
// All specials
const { data } = useSpecials();
// First/featured special
const { special, hasSpecial } = useActiveSpecial();
// Check if specials exist
const { hasSpecials, count } = useHasSpecials();TypeScript
The SDK is fully typed. Use generics for typed content data:
interface FloorplanData {
name: string;
sqft: number;
beds: number;
baths: number;
price: string;
}
// With client
const floorplans = await cms.getContentByType<FloorplanData>('floorplans');
floorplans.forEach(fp => {
console.log(fp.data.name); // TypeScript knows this is a string
console.log(fp.data.sqft); // TypeScript knows this is a number
});
// With hooks
const { data } = useContentByType<FloorplanData>('floorplans');Error Handling
import { ReapAPIError, ReapNetworkError } from '@reap/cms-sdk';
try {
const content = await cms.getContentInstance('sections', 'nonexistent');
} catch (error) {
if (error instanceof ReapAPIError) {
console.log(error.statusCode); // 404
console.log(error.endpoint); // '/content-api/sections/nonexistent'
if (error.isAuthError()) {
// Handle 401 - invalid API key
} else if (error.isNotFoundError()) {
// Handle 404 - content not found
}
} else if (error instanceof ReapNetworkError) {
// Network failure or timeout
console.log(error.originalError);
}
}Examples
Astro SSR
---
// src/pages/index.astro
import { ReapClient } from '@reap/cms-sdk';
const cms = new ReapClient({
baseUrl: import.meta.env.REAP_API_URL,
apiKey: import.meta.env.REAP_API_KEY,
});
interface HeroData {
title: string;
subtitle?: string;
}
const hero = await cms.getContentInstance<HeroData>('sections', 'hero');
const globals = await cms.getGlobals();
---
<html>
<head>
<title>{globals.name}</title>
</head>
<body>
<h1>{hero.data.title}</h1>
{hero.data.subtitle && <p>{hero.data.subtitle}</p>}
</body>
</html>React Form Submission
import { useForms } from '@reap/cms-sdk/react';
import ReCAPTCHA from 'react-google-recaptcha';
function ContactForm() {
const { data, submitForm, isSubmitting, submitError } = useForms();
const [formData, setFormData] = useState({});
const [token, setToken] = useState('');
const form = data?.forms.find(f => f.is_default);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form) return;
try {
const result = await submitForm(form.id, formData, token);
if (result.success) {
alert('Thanks for your submission!');
if (result.redirect_url) {
window.location.href = result.redirect_url;
}
}
} catch (err) {
// Error is also available via submitError
}
};
if (!form) return null;
return (
<form onSubmit={handleSubmit}>
{form.fields.map(field => (
<div key={field.id}>
<label>{field.label}</label>
<input
type={field.type === 'textarea' ? undefined : field.type}
required={field.required}
onChange={e => setFormData(prev => ({
...prev,
[field.label]: e.target.value
}))}
/>
</div>
))}
{data?.recaptchaSiteKey && (
<ReCAPTCHA sitekey={data.recaptchaSiteKey} onChange={setToken} />
)}
<button type="submit" disabled={isSubmitting || !token}>
{isSubmitting ? 'Sending...' : 'Submit'}
</button>
{submitError && <p className="error">{submitError.message}</p>}
</form>
);
}License
MIT
