npm package: @renatoalmeida/headless-cms
Description: TypeScript client library for the Headless CMS API
Author: Renato Almeida
Latest version: 1.0.2

# Headless CMS Client SDK

A TypeScript client library for consuming the Headless CMS API.

## Installation

```bash
npm install @your-org/headless-cms-client
# or
yarn add @your-org/headless-cms-client
```

## Quick Start

```typescript
import { createClient } from '@your-org/headless-cms-client';

// Create a client instance
const client = createClient({
  baseUrl: 'https://your-tenant.headless-cms.com',
  apiKey: 'your-api-key-here', // Optional for public content
  timeout: 30000, // Optional, defaults to 30s
  retries: 3, // Optional, defaults to 3
});

// Get all content types
const contentTypes = await client.getContentTypes();
console.log(contentTypes.data);

// Get published blog posts
const blogPosts = await client.getPublishedContent('blog_post', {
  limit: 10,
  sort: 'createdAt',
  order: 'desc',
});

// Get a specific blog post by slug
const post = await client.getContentItem('blog_post', 'my-first-post');

// Search across all content
const searchResults = await client.search('javascript tutorial', {
  limit: 20,
});
```

## API Reference

### Client Configuration

```typescript
interface ClientConfig {
  baseUrl: string; // Your CMS base URL
  apiKey?: string; // API key for authentication (optional for public content)
  timeout?: number; // Request timeout in milliseconds (default: 30000)
  retries?: number; // Number of retries for failed requests (default: 3)
}
```

### Content Types

#### Get All Content Types

```typescript
const response = await client.getContentTypes();
// Returns: { data: ContentType[], meta: any }
```

#### Get Single Content Type

```typescript
const response = await client.getContentType('blog_post');
// Returns: { data: ContentType, meta: any }
```

### Content

#### Get Content Items

```typescript
const options = {
  page: 1, // Page number (default: 1)
  limit: 10, // Items per page (default: 10, max: 100)
  search: 'keyword', // Search query
  status: 'published', // Content status: 'published', 'draft', 'archived', 'all'
  sort: 'createdAt', // Sort field
  order: 'desc', // Sort order: 'asc' or 'desc'
  category: 'tech', // Filter by category
  tag: 'javascript', // Filter by tag
  before: '2024-01-01', // Content created before date
  after: '2023-01-01', // Content created after date
};

const response = await client.getContent('blog_post', options);
```

#### Get Single Content Item

```typescript
// By ID
const post = await client.getContentItem('blog_post', 123);

// By slug
const post = await client.getContentItem('blog_post', 'my-post-slug');
```

#### Search Content

```typescript
const results = await client.search('javascript tutorial', {
  limit: 20,
  page: 1,
});
```

### GraphQL

#### Execute GraphQL Query

```typescript
const query = `
  query GetBlogPosts($first: Int) {
    content(contentType: "blog_post", first: $first) {
      nodes {
        id
        values
        createdAt
      }
      totalCount
    }
  }
`;

const result = await client.graphql(query, { first: 10 });
```

### Convenience Methods

#### Get Published Content

```typescript
// Only gets published content
const posts = await client.getPublishedContent('blog_post', {
  limit: 10,
  sort: 'createdAt',
  order: 'desc',
});
```

#### Get Recent Content

```typescript
// Get recent content across all types
const recentContent = await client.getRecentContent(20);

// Get recent content from specific types
const recentContent = await client.getRecentContent(20, ['blog_post', 'news']);
```

#### Get Content by Taxonomy

```typescript
// Get content by category
const techPosts = await client.getContentByTaxonomy(
  'blog_post',
  'category',
  'technology'
);

// Get content by tag
const jsPosts = await client.getContentByTaxonomy(
  'blog_post',
  'tag',
  'javascript'
);
```

## Utility Functions

### Extract Field Values

```typescript
import {
  getFieldValue,
  getRelatedContent,
  formatContent,
} from '@your-org/headless-cms-client';

const content = await client.getContentItem('blog_post', 123);

// Get specific field value
const title = getFieldValue(content.data, 'title');
const excerpt = getFieldValue(content.data, 'excerpt');

// Get related content
const relatedPosts = getRelatedContent(content.data, 'related_posts');
const categories = getRelatedContent(content.data, 'categories');

// Format content for display
const formatted = formatContent(content.data);
console.log(formatted.title, formatted.excerpt, formatted.publishedAt);
```

### Type-Safe Content

```typescript
import { getTypedContent } from '@your-org/headless-cms-client';

interface BlogPost {
  title: string;
  content: string;
  excerpt?: string;
  featured_image?: string;
}

const content = await client.getContentItem('blog_post', 123);

// Type-safe content access
const blogPost = getTypedContent<BlogPost>(content.data);
console.log(blogPost.title); // TypeScript knows this is a string
```

## Error Handling

The client automatically handles:

- **Retries**: Failed requests are retried up to 3 times with exponential backoff
- **Timeouts**: Requests timeout after 30 seconds by default
- **Network Errors**: Automatic retry for network issues
- **Rate Limiting**: Respects rate limit headers and retries after delay

```typescript
try {
  const content = await client.getContent('blog_post');
} catch (error) {
  if (error.message.includes('Rate limit')) {
    // Handle rate limiting
    console.log('Rate limited, try again later');
  } else if (error.message.includes('timeout')) {
    // Handle timeout
    console.log('Request timed out');
  } else {
    // Handle other errors
    console.error('API Error:', error.message);
  }
}
```

## React Integration

For React applications, you can create custom hooks:

```typescript
import { useState, useEffect } from 'react'
import { HeadlessCmsClient } from '@your-org/headless-cms-client'

function useContent(client: HeadlessCmsClient, contentType: string, options = {}) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    async function fetchContent() {
      try {
        setLoading(true)
        const response = await client.getPublishedContent(contentType, options)
        setData(response.data)
      } catch (err) {
        setError(err)
      } finally {
        setLoading(false)
      }
    }

    fetchContent()
  }, [contentType, JSON.stringify(options)])

  return { data, loading, error }
}

// Usage in component
function BlogList() {
  const client = createClient({ baseUrl: 'https://your-cms.com' })
  const { data: posts, loading, error } = useContent(client, 'blog_post', {
    limit: 10,
    sort: 'createdAt',
    order: 'desc'
  })

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error.message}</div>

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{getFieldValue(post, 'title')}</h2>
          <p>{getFieldValue(post, 'excerpt')}</p>
        </article>
      ))}
    </div>
  )
}
```

## Next.js Integration

For Next.js applications with SSR/SSG:

```typescript
// pages/blog/[slug].js
import { createClient, getFieldValue } from '@your-org/headless-cms-client'

export async function getStaticProps({ params }) {
  const client = createClient({
    baseUrl: process.env.CMS_BASE_URL,
    apiKey: process.env.CMS_API_KEY
  })

  try {
    const post = await client.getContentItem('blog_post', params.slug)

    return {
      props: {
        post: post.data
      },
      revalidate: 60 // Revalidate every minute
    }
  } catch (error) {
    return {
      notFound: true
    }
  }
}

export async function getStaticPaths() {
  const client = createClient({
    baseUrl: process.env.CMS_BASE_URL,
    apiKey: process.env.CMS_API_KEY
  })

  const posts = await client.getPublishedContent('blog_post', { limit: 100 })

  const paths = posts.data.map(post => ({
    params: { slug: getFieldValue(post, 'slug') }
  }))

  return {
    paths,
    fallback: 'blocking'
  }
}

export default function BlogPost({ post }) {
  return (
    <article>
      <h1>{getFieldValue(post, 'title')}</h1>
      <div dangerouslySetInnerHTML={{ __html: getFieldValue(post, 'content') }} />
    </article>
  )
}
```

## Advanced Usage

### Custom Headers

```typescript
const client = createClient({
  baseUrl: 'https://your-cms.com',
  apiKey: 'your-api-key',
});

// The client automatically adds these headers:
// - Content-Type: application/json
// - Authorization: Bearer {apiKey}
// - User-Agent: headless-cms-client/1.0
```

### Environment Configuration

```typescript
// .env.local
CMS_BASE_URL=https://your-tenant.headless-cms.com
CMS_API_KEY=ak_your_api_key_here

// config/cms.ts
export const cmsClient = createClient({
  baseUrl: process.env.CMS_BASE_URL!,
  apiKey: process.env.CMS_API_KEY,
  timeout: parseInt(process.env.CMS_TIMEOUT || '30000'),
  retries: parseInt(process.env.CMS_RETRIES || '3')
})
```

## TypeScript Support

The client is written in TypeScript and provides full type safety:

```typescript
// All responses are properly typed
const contentTypes: { data: ContentType[]; meta: any } =
  await client.getContentTypes();
const content: PaginatedResponse<ContentItem> =
  await client.getContent('blog_post');
const item: { data: ContentItem; meta: any } = await client.getContentItem(
  'blog_post',
  123
);

// Content fields are accessible but not strictly typed (since they're dynamic)
const title: any = getFieldValue(content.data[0], 'title');

// Use type assertion for known content structures
interface BlogPost {
  title: string;
  content: string;
  excerpt: string;
}

const typedPost = getTypedContent<BlogPost>(content.data[0]);
// Now typedPost.title is properly typed as string
```