npm package: @mhbdev/unicms
Description: Typed SDK for UniCMS public API
Latest version: 0.2.0

# @mhbdev/unicms

Typed SDK for the UniCMS public API (`/api/public/v1/*`).

## Install

```bash
pnpm add @mhbdev/unicms
```

## Create client

```ts
import { createUniCmsClient } from '@mhbdev/unicms'

const cms = createUniCmsClient({
  baseUrl: 'https://cms.example.com',
  tenant: { slug: 'acme' },
})
```

Use domain-based tenant scope instead:

```ts
const cms = createUniCmsClient({
  baseUrl: 'https://cms.example.com',
  tenant: { domain: 'acme.example.com' },
})
```

## Optional auth headers (private tenants)

```ts
const cms = createUniCmsClient({
  baseUrl: 'https://cms.example.com',
  tenant: { slug: 'acme' },
  headers: async () => ({
    Authorization: `Bearer ${await getToken()}`,
  }),
})
```

## API

```ts
await cms.getSiteSettings()
await cms.getPageBySlug('home')
await cms.getPageBySlug('pricing', { includeRawContent: true })

await cms.listPosts({ page: 1, limit: 10, q: 'cms', categorySlug: 'news', tagSlug: 'payload' })
await cms.getPostBySlug('hello-world')
await cms.getPostBySlug('hello-world', { includeRawContent: true })

await cms.listFaqs()
await cms.listFaqs({ includeRawContent: true })

await cms.listCategories()
await cms.listTags()

await cms.externalUsersLogin({
  username: 'tenant-admin',
  password: 'secret',
})
```

## Generic typed requests

Use `request` for advanced/custom endpoints with schema-validated responses:

```ts
import { z } from 'zod'

const response = await cms.request({
  path: '/api/public/v1/posts',
  query: { page: 1, limit: 5 },
  schema: z.object({
    data: z.object({
      docs: z.array(z.object({ id: z.number(), slug: z.string() })),
    }),
  }),
})
```

## React / Next.js integration

Install React peer deps if needed:

```bash
pnpm add react react-dom
```

Use the React entrypoint:

```tsx
'use client'

import { createUniCmsClient } from '@mhbdev/unicms'
import { UniCmsProvider, usePageBySlugQuery, useSiteSettingsQuery } from '@mhbdev/unicms/react'

const client = createUniCmsClient({
  baseUrl: 'https://cms.example.com',
  tenant: { slug: 'acme' },
})

function HomePage() {
  const siteSettings = useSiteSettingsQuery()
  const page = usePageBySlugQuery({ slug: 'home' })

  if (siteSettings.isLoading || page.isLoading) return <p>Loading...</p>
  if (siteSettings.error || page.error) return <p>Failed to load</p>

  return <h1>{page.data?.title}</h1>
}

export default function App() {
  return (
    <UniCmsProvider
      client={client}
      defaultQueryOptions={{ staleTimeMs: 30_000 }}
      namespace="acme-frontend"
    >
      <HomePage />
    </UniCmsProvider>
  )
}
```

Available hooks:
- `useUniCmsClient`
- `useUniCmsQuery`
- `useUniCmsMutation`
- `useUniCmsInvalidate`
- `useSiteSettingsQuery`
- `usePageBySlugQuery`
- `usePostsQuery`
- `usePostBySlugQuery`
- `useFaqsQuery`
- `useCategoriesQuery`
- `useTagsQuery`
- `useExternalUsersLoginMutation`

## Next App Router SSR helpers

Use `@mhbdev/unicms/next` inside Server Components, Route Handlers, and Server Actions:

```ts
import { createNextAppRouterUniCmsClient } from '@mhbdev/unicms/next'

const cms = await createNextAppRouterUniCmsClient({
  tenant: { slug: 'acme' },
  // Optional. If omitted, it tries:
  // UNICMS_BASE_URL -> NEXT_PUBLIC_SERVER_URL -> PAYLOAD_PUBLIC_SERVER_URL -> NEXT_PUBLIC_SITE_URL
  baseUrl: 'https://cms.example.com',
})
```

### Server Component example

```tsx
import { createNextAppRouterUniCmsClient } from '@mhbdev/unicms/next'

export default async function Page() {
  const cms = await createNextAppRouterUniCmsClient({
    tenant: { slug: 'acme' },
  })

  const settings = await cms.getSiteSettings()
  return <h1>{settings.siteName}</h1>
}
```

### Route Handler example (request-scoped forwarding)

```ts
import { createNextAppRouterUniCmsClient } from '@mhbdev/unicms/next'

export async function GET(request: Request) {
  const cms = await createNextAppRouterUniCmsClient({
    request, // forwards selected request headers + cookie header by default
    tenant: { domain: 'acme.example.com' },
  })

  const posts = await cms.listPosts({ page: 1, limit: 10 })
  return Response.json(posts)
}
```

### Custom forwarding

```ts
const cms = await createNextAppRouterUniCmsClient({
  tenant: { slug: 'acme' },
  includeCookies: true,
  requestHeaders: ['authorization', 'x-request-id'],
})
```

## Error handling

SDK methods throw `UniCmsError`:

```ts
import { UniCmsError } from '@mhbdev/unicms'

try {
  await cms.getPageBySlug('missing-page')
} catch (error) {
  if (error instanceof UniCmsError) {
    console.error(error.code, error.status, error.message, error.details)
  }
}
```

Error codes:
- `BAD_REQUEST`
- `UNAUTHORIZED`
- `FORBIDDEN`
- `NOT_FOUND`
- `VALIDATION_ERROR`
- `NETWORK_ERROR`
- `PARSE_ERROR`
- `UNKNOWN`

## Pagination

`listPosts` returns:

```ts
type Paginated<T> = {
  docs: T[]
  pagination: {
    page: number
    limit: number
    totalDocs: number
    totalPages: number
    hasNextPage: boolean
    hasPrevPage: boolean
    nextPage: number | null
    prevPage: number | null
  }
}
```

Defaults: `page=1`, `limit=10`, max `limit=50`.

## Compatibility

- SDK targets UniCMS public API v1.
- Endpoints are expected at `/api/public/v1/*`.
- v1 returns published content only.

## Release notes

- `0.1.x`: Initial v1 contract-first release.