@conloca/content-api-client
Overview
@conloca/content-api-client provides React Query hooks and an HTTP client for interacting with the Conloca Content
API. It powers the CMS admin UI and can be used in any React application that needs to read or write CMS content.
Features:
- React Query hooks with automatic caching and invalidation
- ETag-based optimistic concurrency control
- Asset management hooks (upload, delete, move, folders)
- Git operations hooks (commit, push, pull, status)
- Content inheritance hooks (mark reviewed, break/revert inheritance) (dev branch preview)
- Test utilities for unit and integration testing
Installation
bun add @conloca/content-api-client
Entry Points
@conloca/content-api-client (main)
React hooks, HTTP client, and re-exported types.
import { useContent, useSitePages, useBlocks, ContentAPIClient, StaleWriteError } from '@conloca/content-api-client';
@conloca/content-api-client/testing
Test utilities for mocking the content API in tests.
import { setupFetchMock, MockAPIResponses, API_ROUTES } from '@conloca/content-api-client/testing';
Content Hooks
Query Hooks
| Hook | Signature | Purpose |
|---|---|---|
useContent | (id: string) | Fetch content entry by ID |
useLocalizedContent | (id: string, locale: string) | Fetch single-locale content |
useSitePages | (site: string, locale?: string) | List pages for a site |
usePageByPathname | (site: string, pathname: string, locale?: string) | Find page by URL pathname |
usePathnameAvailability | (site: string, pathname: string, excludeId?: string) | Check if pathname is available |
useBlocks | (collection?: string, locale?: string) | List blocks |
useBlockByName | (name: string, collection?: string, locale?: string) | Fetch single block by name |
useData | (collection?: string, locale?: string) | List data entries |
useDataByName | (name: string, collection: string, locale?: string) | Fetch data entry by name |
useDataNameAvailability | (name: string, collection: string, excludeId?: string) | Check data name availability |
useDataCollections | () | List available data collections |
useAllContent | (filters?: GlobalFilters) | List all content with filters |
useUntranslatedContent | (targetLocale: string, excludeSites?: string[], includeUnpublished?: boolean) | Find untranslated content |
useSitesConfig | () | Fetch multi-site configuration |
useCurrentUser | () | Get authenticated user info |
Mutation Hooks
| Hook | Purpose |
|---|---|
useCreateContent() | Create new content (page, block, or data) |
useUpdateLocalized() | Update localized content with ETag validation |
useDeleteContent() | Delete content or a specific locale |
useDeleteLocalized() | Delete a single locale from content |
useMovePage() | Move a page to a new pathname |
useBatchUpdate() | Batch update multiple content entries |
Asset Hooks
| Hook | Signature | Purpose |
|---|---|---|
useAssets | () | List all assets |
useAsset | (filename: string) | Fetch single asset metadata |
useUploadAsset | () | Upload asset mutation |
useImportAssetUrl | () | Import asset from URL mutation |
useDeleteAsset | () | Delete asset mutation |
useBulkDeleteAssets | () | Delete multiple assets mutation |
useAssetFolders | (path?: string) | List folder contents |
useCreateFolder | () | Create folder mutation |
useUpdateAssetMetadata | () | Update asset alt text / tags |
useAssetUsage | (filename: string | null) | Find where an asset is referenced |
useFolderTree | () | Get full folder tree structure |
useMoveAssets | () | Move assets between folders |
MDX & Data Hooks
| Hook | Parameters | Purpose |
|---|---|---|
useCompileMDX | ({ mdxContent, cacheKey }) | Compile MDX content on the server |
useDataContext | (pageId?: string) | Get data context for resolveData hooks |
Git Hooks
| Hook | Purpose |
|---|---|
useGitStatus() | Get current git status (changes, branch, remote) |
useCommitChanges() | Commit content changes mutation |
usePushChanges() | Push to remote mutation |
usePullChanges() | Pull from remote mutation |
Inheritance Hooks
| Hook | Purpose |
|---|---|
useNeedsReview(locale, kind?) | List content needing inheritance review |
useMarkReviewed() | Mark content as reviewed after parent change |
useBreakInheritance() | Break inheritance link for content |
useRevertToInherited() | Revert content to inherited version |
Schemas
@conloca/content-api-client re-exports the Zod schemas from @conloca/content-api for use in CMS form generation and
content validation.
| Export | Type | Description |
|---|---|---|
blockEditableSchema | ZodObject | Zod schema for editable block metadata (title, description, category, tags). Use with .extend() to add block-specific fields. |
dataEditableSchema | ZodObject | Zod schema for editable data entry metadata (title, description, category, tags). Parallel to blockEditableSchema for data collections. |
Both schemas expose the same four organizational fields:
| Field | Type | Description |
|---|---|---|
title | string | Display name for the block or data entry |
description | string (optional) | Brief description |
category | string (optional) | Category for organizing entries |
tags | string[] (optional) | Tags for filtering and organization |
import { blockEditableSchema, dataEditableSchema } from '@conloca/content-api-client';
// Extend to add custom fields to a data schema
const teamMemberSchema = dataEditableSchema.extend({
role: z.string().describe('Job title'),
avatar: z.string().url().optional().describe('Profile image URL'),
});
ContentAPIClient
The HTTP client used by all hooks. Can also be used directly for non-React code.
import { ContentAPIClient } from '@conloca/content-api-client';
const client = new ContentAPIClient({
baseUrl: '/__cms/api', // Astro integration default (library default is '/__conloca/api')
fetch: customFetch, // optional custom fetch
});
The library default baseUrl is '/__conloca/api'. When used with @conloca/astro-cms, the standard CMS API mount
path is '/__cms/api' — pass that explicitly (or use the CMS SPA defaults, which already set this). For cross-origin
setups, provide the full base URL (e.g., https://cms.example.com/__cms/api).
Client Methods
| Method | Return Type | Purpose |
|---|---|---|
getContent(id) | ContentEntry | null | Fetch full content entry |
getLocalized(id, locale) | LocalizedEntry | null | Fetch single-locale content |
createContent(data) | CreateResult | Create new content |
updateLocalized(input) | UpdateResult | Update with ETag validation |
deleteContent(id, etag) | DeleteResult | Delete entire content |
deleteLocalized(input) | DeleteResult | Delete single locale |
getSitePages(site, locale?) | ContentListResult | List site pages |
getPageByPathname(site, pathname, locale?) | ContentManifest | null | Find page by pathname |
isPathnameAvailable(site, pathname, excludeId?) | boolean | Check pathname availability |
movePage(site, id, pathname, locale, etag) | MoveResult | Move page to new pathname |
getBlocks(collection?, locale?) | ContentListResult | List blocks |
getBlockByName(name, collection?, locale?) | ContentManifest | null | Get block by name |
getData(collection?, locale?) | ContentListResult | List data entries |
getDataByName(name, collection, locale?) | ContentManifest | null | Get data by name |
listAllContent(filters?) | ContentListResult | List all content |
batchUpdate(operations) | BatchResult | Batch update |
getGitStatus() | GitStatus | Git repository status |
commitChanges(message?) | GitCommitResult | Commit changes |
pushChanges() | GitPushResult | Push to remote |
pullChanges() | GitPullResult | Pull from remote |
listAssets() | { assets: AssetEntry[] } | List all assets |
uploadAsset(formData) | { asset: AssetEntry } | Upload asset |
deleteAsset(filename) | { success: boolean } | Delete asset |
getCurrentUser() | { authenticated, user? } | Get authenticated user |
isDataNameAvailable(name, collection, excludeId?) | boolean | Check data name availability |
getDataCollections() | string[] | List data collections |
findUntranslatedContent(targetLocale, excludeSites?, includeUnpublished?) | ContentListResult | Find untranslated content |
getSitesConfig() | SitesConfig | Get multi-site configuration |
getAsset(filename) | AssetEntry | null | Get single asset metadata |
importAssetUrl(url, alt?, folder?) | { asset: AssetEntry } | Import asset from URL |
listFolder(path?) | FolderListing | List folder contents |
createFolder(path) | void | Create new folder |
updateAssetMetadata(filename, updates) | AssetEntry | Update asset alt text / tags |
getAssetUsage(filename) | AssetUsage[] | Find where asset is used |
moveAssets(filenames, sourceFolder, targetFolder) | { success, moved? } | Move assets between folders |
getFolderTree() | { tree: FolderTreeNode[] } | Get folder hierarchy |
Error Handling
import { StaleWriteError, APIClientError } from '@conloca/content-api-client';
try {
await client.updateLocalized({ id, locale, data, etag });
} catch (error) {
if (error instanceof StaleWriteError) {
// Content was modified by another user
// error.data.currentEtag contains the latest ETag
console.log('Conflict - latest ETag:', error.data.currentEtag);
}
if (error instanceof APIClientError) {
console.log('API error:', error.code, error.details);
}
}
Client Configuration
Global Client
The hooks use a global client instance. Configure it before rendering:
import { setContentAPIClient, ContentAPIClient } from '@conloca/content-api-client';
setContentAPIClient(new ContentAPIClient({ baseUrl: '/api' }));
Examples
Listing Pages
import { useSitePages } from '@conloca/content-api-client'
function PageList({ site }) {
const { data, isLoading, error } = useSitePages(site, 'en')
if (isLoading) return <p>Loading...</p>
if (error) return <p>Error: {error.message}</p>
return (
<ul>
{data?.items.map((page) => (
<li key={page.id}>
{page.locales.en?.pathname || page.id}
</li>
))}
</ul>
)
}
Creating Content
import { useCreateContent } from '@conloca/content-api-client'
function CreatePageButton({ site }) {
const createContent = useCreateContent()
const handleCreate = () => {
createContent.mutate({
kind: 'page',
site,
collection: 'pages',
type: 'puck',
locales: {
en: {
pathname: '/new-page',
meta: { title: 'New Page' },
content: { puckData: { root: { props: {} }, content: [], zones: {} } },
},
},
})
}
return <button onClick={handleCreate}>Create Page</button>
}
Testing with Mock API
import { describe, test, expect, mock } from 'bun:test';
import { setupFetchMock, mockContentEntry } from '@conloca/content-api-client/testing';
import { ContentAPIClient } from '@conloca/content-api-client';
describe('content operations', () => {
test('fetches content by ID', async () => {
const { responses } = setupFetchMock(mock);
const entry = mockContentEntry({ id: 'page-1' });
responses.mockGetContent(entry);
const client = new ContentAPIClient();
const result = await client.getContent('page-1');
expect(result?.id).toBe('page-1');
});
});
The @conloca/content-api-client/testing entry point exports:
| Export | Purpose |
|---|---|
setupFetchMock(mockFn) | Set up a mock fetch with MockAPIResponses helper |
MockAPIResponses | Chainable mock response builder (mockGetContent, mockStaleWrite, etc.) |
API_ROUTES | Route pattern constants for matching API calls |
createMockFetch(mockFn) | Low-level mock fetch factory |
jsonResponse(body, options?) | Create a JSON Response object for tests |
mockContentEntry(overrides?) | Create a mock ContentEntry |
mockContentManifest(overrides?) | Create a mock ContentManifest |
mockLocalizedEntry(overrides?) | Create a mock LocalizedEntry |
mockCreateResult(overrides?) | Create a mock CreateResult |
mockUpdateResult(overrides?) | Create a mock UpdateResult |
mockDeleteResult(overrides?) | Create a mock DeleteResult |
mockBatchResult(overrides?) | Create a mock BatchResult |