A lightweight, framework-agnostic TypeScript client for the ContentEdge headless CMS. It provides a clean, typed interface over the HTTP API with public API key authentication, robust pagination helpers, asset/file utilities, and structured errors—without embedding project-specific domain models.
This SDK models the wire contract via a generic ContentDto<C> where C represents your custom fields. It does not import or depend on the CMS server code.
![]()
CodeSociety is our consulting & contracting arm — specializing in IT architecture, XML authoring systems, FontoXML integration, and TerminusDB consulting. We build structured content platforms and data solutions that power digital publishing.
- Generic content model:
ContentDto<C>with consumer-defined custom fields - Public API key authentication: Simple header-based authentication
- Service layer pattern: Centralized API client with interceptors
- React Query integration: Optional query factory and hooks
- Resilient pagination:
fetchAllContentaggregates pages with dedupe and safe stop - Asset/file helpers:
buildAssetUrland safedownloadFile - Normalization utilities: Transform API responses to normalized structures
- Structured error model:
CmsErrorwith status and response data - Framework-agnostic: Core services work anywhere; React Query layer is optional
- TypeScript-first: Full type safety with generics
npm install @codesocietyou/contentedge-cms-sdk
# or
yarn add @codesocietyou/contentedge-cms-sdk
# or
pnpm add @codesocietyou/contentedge-cms-sdkFor React Query integration, also install:
npm install @tanstack/react-queryimport { createApiClient } from '@codesocietyou/contentedge-cms-sdk';
// Initialize once at app startup
createApiClient({
baseUrl: 'https://api.contentedge.com',
fileBaseUrl: 'https://cdn.contentedge.com', // optional
apiKey: 'your-api-key', // optional
tenant: 'your-tenant', // optional
timeoutMs: 30_000, // optional (default: 30s)
});import { fetchContentByType } from '@codesocietyou/contentedge-cms-sdk';
// Fetch paginated content
const response = await fetchContentByType({
type: 'NEWS',
page: 0,
size: 10,
sortBy: 'id',
direction: 'DESC',
filters: { publicationType: 'GAMEHEARTS' }, // arbitrary filters
});
const items = response.data.content;import { useQuery } from '@tanstack/react-query';
import { contentQueries } from '@codesocietyou/contentedge-cms-sdk';
function NewsList() {
const { data, isLoading, error } = useQuery(
contentQueries.list({ type: 'NEWS', page: 0, size: 10 })
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data.data.content.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
}import {
normalizeContentItem,
fetchContentByType
} from '@codesocietyou/contentedge-cms-sdk';
// Fetch and normalize
const response = await fetchContentByType({ type: 'NEWS' });
const normalized = response.data.content.map(normalizeContentItem);
// normalized items have resolved asset URLs and standardized fields
console.log(normalized[0].insideImage); // "https://cdn.contentedge.com/images/news.jpg"
console.log(normalized[0].pdfPath); // "https://cdn.contentedge.com/files/doc.pdf"Initialize the SDK with your CMS configuration. Call this once at app startup.
interface SdkConfig {
baseUrl: string; // CMS API base URL (required)
fileBaseUrl?: string; // Preferred file/asset base
apiKey?: string; // API key for authentication
tenant?: string; // Tenant identifier (sent as X-Tenant header)
timeoutMs?: number; // Request timeout (default: 30000)
logger?: {
debug?: (...args: unknown[]) => void;
warn?: (...args: unknown[]) => void;
error?: (...args: unknown[]) => void;
};
}Fetch paginated content by type with filters and sorting.
interface ContentListParams {
type?: string; // Content type (default: 'ALL')
page?: number; // Page number (0-indexed)
size?: number; // Items per page
sortBy?: string; // Sort field (default: 'id')
direction?: 'ASC' | 'DESC'; // Sort direction (default: 'DESC')
filters?: Record<string, any>; // Arbitrary query filters
}Fetch a single content item by ID.
Fetch all content items across multiple pages with automatic pagination.
interface FetchAllOptions<C, T> {
mapItem?: (item: ContentDto<C>) => T; // Transform each item
dedupeBy?: (item: T) => string | number; // Dedupe key extractor
hardStopMaxPages?: number; // Safety limit (default: 20)
}Download a file from a given path (with proper authentication for CMS files).
Query options factory for use with useQuery:
// Paginated list
contentQueries.list({ type: 'NEWS', page: 0, size: 10 })
// Single item
contentQueries.detail(123)
// Fetch all (across pages)
contentQueries.listAll({ type: 'NEWS', size: 100 }, { mapItem: normalizeContentItem })// List hook
const { data } = useContentList({ type: 'NEWS', page: 0, size: 10 });
// Detail hook
const { data } = useContentDetail(123);
// Fetch all hook
const { data } = useContentAll({ type: 'NEWS' });
// Prefetch utilities
const { prefetchList, prefetchDetail, invalidateLists } = useContentPrefetch();Hierarchical query key factory for manual cache manipulation:
import { contentKeys } from '@codesocietyou/contentedge-cms-sdk';
// Invalidate all lists
queryClient.invalidateQueries({ queryKey: contentKeys.lists() });
// Invalidate specific detail
queryClient.invalidateQueries({ queryKey: contentKeys.detail(123) });Transform a content item to a normalized structure with resolved asset URLs:
interface NormalizedContentItem {
id: number;
title: string;
text: string;
type: string;
insideImage: string; // Resolved URL
outsideImage: string; // Resolved URL
pdfPath: string | null; // Resolved URL (type-aware)
references: string | null;
citation: string | null;
abstract: string | null;
team: string | null;
publicationType: string | null;
fake: boolean | null;
}Build an asset URL using the SDK's configured base URLs.
import { CmsError } from '@codesocietyou/contentedge-cms-sdk';
try {
await fetchContentByType({ type: 'NEWS' });
} catch (e) {
if (e instanceof CmsError) {
console.error('CMS error:', e.status, e.data);
// e.status: HTTP status code
// e.data: Response body (if any)
} else {
console.error('Unknown error:', e);
}
}Define your own custom fields type for full type safety:
interface MyCustomFields {
title: string;
text: string;
author?: string;
tags?: string[];
publishedAt?: string;
}
// Use with type parameter
const response = await fetchContentByType<MyCustomFields>({ type: 'ARTICLE' });
const items = response.data.content; // ContentDto<MyCustomFields>[]interface MyNormalizedItem {
id: number;
title: string;
author: string;
tags: string[];
}
function myNormalize(item: ContentDto<MyCustomFields>): MyNormalizedItem {
return {
id: item.id,
title: item.customFields.title || item.title,
author: item.customFields.author || 'Unknown',
tags: item.customFields.tags || [],
};
}
// Use with fetchAllContent
const items = await fetchAllContent<MyCustomFields, MyNormalizedItem>(
{ type: 'ARTICLE', size: 100 },
{ mapItem: myNormalize }
);import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createApiClient } from '@codesocietyou/contentedge-cms-sdk';
// Initialize SDK
createApiClient({
baseUrl: process.env.VITE_API_URL!,
apiKey: process.env.VITE_API_KEY,
tenant: 'my-tenant',
});
// Create query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
refetchOnWindowFocus: false,
},
},
});
// Wrap app
function App() {
return (
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
);
}The v1.0.0 release includes breaking changes:
CmsClientclass → Use service functions +createApiClient()KeycloakClientCredentialsAuth→ Use API key authenticationAuthProviderinterface → No longer needed
Before (v0.2.x):
import { CmsClient, KeycloakClientCredentialsAuth } from '@codesocietyou/contentedge-cms-sdk';
const auth = new KeycloakClientCredentialsAuth({
tokenUrl: 'https://auth.example.com/token',
clientId: 'client',
clientSecret: 'secret',
});
const client = new CmsClient({
baseUrl: 'https://api.example.com',
auth,
});
const list = await client.listContent({ type: 'NEWS' });
const detail = await client.getContentById(123);After (v1.0.0):
import {
createApiClient,
fetchContentByType,
fetchContentById
} from '@codesocietyou/contentedge-cms-sdk';
// Initialize once
createApiClient({
baseUrl: 'https://api.example.com',
apiKey: 'your-api-key',
});
// Use service functions
const list = await fetchContentByType({ type: 'NEWS' });
const detail = await fetchContentById(123);- API Keys: Store in environment variables, never commit to source control
- Runtime Configuration: Inject secrets at runtime in production
- CORS: Ensure your CMS API allows requests from your domain
- Rate Limiting: The SDK logs 429 errors; implement retry logic if needed
This SDK is designed specifically for the ContentEdge CMS API. The endpoint paths are fixed as part of the CMS API contract:
GET /content/type/:type- List content by type with paginationGET /content/:id- Get single content item by ID
The baseUrl configuration allows you to connect to different ContentEdge CMS deployments:
Development:
createApiClient({ baseUrl: 'http://localhost:8080/api' });Staging:
createApiClient({ baseUrl: 'https://staging-cms.contentedge.com/api' });Production:
createApiClient({ baseUrl: 'https://cms.contentedge.com/api' });This deployment flexibility is intentional and does not mean the SDK supports different API contracts. All ContentEdge CMS instances use the same endpoint structure.
Contributions are welcome! Please follow the existing code style and add tests for new features.
This project follows Semantic Versioning. Breaking changes bump MAJOR.
MIT
