Skip to content

@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

HookSignaturePurpose
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

HookPurpose
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

HookSignaturePurpose
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

HookParametersPurpose
useCompileMDX({ mdxContent, cacheKey })Compile MDX content on the server
useDataContext(pageId?: string)Get data context for resolveData hooks

Git Hooks

HookPurpose
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

HookPurpose
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.

ExportTypeDescription
blockEditableSchemaZodObjectZod schema for editable block metadata (title, description, category, tags). Use with .extend() to add block-specific fields.
dataEditableSchemaZodObjectZod schema for editable data entry metadata (title, description, category, tags). Parallel to blockEditableSchema for data collections.

Both schemas expose the same four organizational fields:

FieldTypeDescription
titlestringDisplay name for the block or data entry
descriptionstring (optional)Brief description
categorystring (optional)Category for organizing entries
tagsstring[] (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

MethodReturn TypePurpose
getContent(id)ContentEntry | nullFetch full content entry
getLocalized(id, locale)LocalizedEntry | nullFetch single-locale content
createContent(data)CreateResultCreate new content
updateLocalized(input)UpdateResultUpdate with ETag validation
deleteContent(id, etag)DeleteResultDelete entire content
deleteLocalized(input)DeleteResultDelete single locale
getSitePages(site, locale?)ContentListResultList site pages
getPageByPathname(site, pathname, locale?)ContentManifest | nullFind page by pathname
isPathnameAvailable(site, pathname, excludeId?)booleanCheck pathname availability
movePage(site, id, pathname, locale, etag)MoveResultMove page to new pathname
getBlocks(collection?, locale?)ContentListResultList blocks
getBlockByName(name, collection?, locale?)ContentManifest | nullGet block by name
getData(collection?, locale?)ContentListResultList data entries
getDataByName(name, collection, locale?)ContentManifest | nullGet data by name
listAllContent(filters?)ContentListResultList all content
batchUpdate(operations)BatchResultBatch update
getGitStatus()GitStatusGit repository status
commitChanges(message?)GitCommitResultCommit changes
pushChanges()GitPushResultPush to remote
pullChanges()GitPullResultPull 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?)booleanCheck data name availability
getDataCollections()string[]List data collections
findUntranslatedContent(targetLocale, excludeSites?, includeUnpublished?)ContentListResultFind untranslated content
getSitesConfig()SitesConfigGet multi-site configuration
getAsset(filename)AssetEntry | nullGet single asset metadata
importAssetUrl(url, alt?, folder?){ asset: AssetEntry }Import asset from URL
listFolder(path?)FolderListingList folder contents
createFolder(path)voidCreate new folder
updateAssetMetadata(filename, updates)AssetEntryUpdate 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:

ExportPurpose
setupFetchMock(mockFn)Set up a mock fetch with MockAPIResponses helper
MockAPIResponsesChainable mock response builder (mockGetContent, mockStaleWrite, etc.)
API_ROUTESRoute 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