Back to skills
extension
Category: Development & EngineeringNo API key required

cosmic

Build applications with Cosmic headless CMS using the JavaScript SDK. Covers Objects, Media, AI generation (text, images, video), content modeling with Metafields, and queries. Use when working with Cosmic CMS, @cosmicjs/sdk, headless content, or building JAMstack applications.

personAuthor: jakexiaohubgithub

Cosmic CMS

Build content-powered applications with the Cosmic headless CMS.

SDK-First Principle

Always use the @cosmicjs/sdk package. The SDK provides type-safe methods, better error handling, and cleaner syntax than raw REST calls.

bun add @cosmicjs/sdk
# or: npm install @cosmicjs/sdk

Setup

import { createBucketClient } from '@cosmicjs/sdk'

const cosmic = createBucketClient({
  bucketSlug: 'BUCKET_SLUG',
  readKey: 'BUCKET_READ_KEY',
  writeKey: 'BUCKET_WRITE_KEY' // Required for create/update/delete
})

Find keys in Cosmic Dashboard → Bucket → Settings → API Access.

Objects (Content)

Objects are the core content units. Each Object belongs to an Object type (like "posts", "products").

Read Objects

// Get multiple Objects
const posts = await cosmic.objects
  .find({ type: 'posts' })
  .props(['id', 'title', 'slug', 'metadata'])
  .limit(10)

// Get single Object by slug
const post = await cosmic.objects
  .findOne({ type: 'posts', slug: 'hello-world' })
  .props(['title', 'slug', 'metadata'])

// Get single Object by id
const obj = await cosmic.objects
  .findOne({ id: 'object-id' })
  .props(['title', 'metadata'])

Props Syntax

Use GraphQL-style syntax for nested properties:

const props = `{
  id
  slug
  title
  metadata {
    content
    author {
      title
      metadata {
        avatar { imgix_url }
      }
    }
  }
}`
await cosmic.objects.find({ type: 'posts' }).props(props)

Create Object

await cosmic.objects.insertOne({
  title: 'My Post',
  type: 'posts', // Object type SLUG, not title
  metadata: {
    content: 'Post content here...',
    author: 'author-object-id',        // Object relationship: use id
    featured_image: 'image-name.jpg'   // Media: use name property
  }
})

Update Object

await cosmic.objects.updateOne('object-id', {
  title: 'Updated Title',
  metadata: {
    featured: true,
    categories: ['cat1-id', 'cat2-id'] // Multiple objects: array of ids
  }
})

Delete Object

await cosmic.objects.deleteOne('object-id')

Batch Operations

Create, update, and delete multiple Objects in a single call (max 25 operations). Each operation succeeds or fails independently.

// Using the SDK
const result = await cosmic.objects.batch([
  { method: 'add', object: { title: 'Post 1', type: 'posts', metadata: { content: '...' } } },
  { method: 'add', object: { title: 'Post 2', type: 'posts', metadata: { content: '...' } } },
  { method: 'edit', object_id: 'OBJECT_ID', object: { title: 'Updated' } },
  { method: 'delete', object_id: 'OBJECT_ID_2' },
])
// result.operations: [{ method, status, object/message }, ...]

Object Types

Define content structure with Object types.

// List all Object types
const types = await cosmic.objectTypes.find()

// Get single Object type
const blogType = await cosmic.objectTypes.findOne('posts')

// Create Object type with Metafields
await cosmic.objectTypes.insertOne({
  title: 'Blog Posts',
  slug: 'posts',
  singular: 'Post',
  emoji: '📝',
  metafields: [
    { title: 'Content', key: 'content', type: 'markdown', required: true },
    { title: 'Image', key: 'image', type: 'file', media_validation_type: 'image' },
    { title: 'Author', key: 'author', type: 'object', object_type: 'authors' },
    { title: 'Tags', key: 'tags', type: 'objects', object_type: 'tags' }
  ]
})

Metafield Types

| Type | Description | Value Format | |------|-------------|--------------| | text | Single line text | string | | textarea | Multi-line text | string | | markdown | Markdown editor | string | | html-textarea | Rich HTML editor | string | | number | Numeric value | number | | date | Date picker | "YYYY-MM-DD" | | switch | Boolean toggle | true/false | | select | Single selection (preferred) | string | | multi-select | Multiple selection | string[] | | select-dropdown | Dropdown selection (deprecated, use select) | {key: string, value: string} | | radio-buttons | Radio selection | string | | check-boxes | Multiple selection | string[] | | file | Single media | Media name | | files | Multiple media | Media name[] | | object | Single relation | Object id | | objects | Multiple relations | Object id[] | | json | JSON data | object | | color | Color picker | "#hex" | | repeater | Repeatable group | array | | parent | Nested group | object |

Metafield Validation

Metafields support validation properties to enforce data quality:

| Property | Type | Description | |----------|------|-------------| | required | boolean | A value must be provided | | unique | boolean | Value must be unique across all Objects of the same type. Applies to top-level text, textarea, number, date, select Metafields (not supported inside Parent or Repeater groups) | | show_when | object | Conditional visibility: { key, op, value }. Show field when sibling field matches condition. Ops: eq, neq, exists, not_exists. Hidden fields skip required validation. Top-level only. | | regex | string | Restrict value to match a regular expression | | regex_message | string | Message shown when regex validation fails | | minlength | number | Minimum character length (text, textarea) | | maxlength | number | Maximum character length (text, textarea) |

// Object type with validation
await cosmic.objectTypes.insertOne({
  title: 'Contacts',
  slug: 'contacts',
  metafields: [
    { title: 'Email', key: 'email', type: 'text', required: true, unique: true },
    { title: 'Name', key: 'name', type: 'text', required: true, minlength: 2 }
  ]
})

Queries

Filter content using MongoDB-style queries:

// Basic filter
await cosmic.objects.find({
  type: 'products',
  'metadata.category': 'electronics'
})

// Comparison operators
await cosmic.objects.find({
  type: 'products',
  'metadata.price': { $lt: 100 }    // Less than
  // $gt, $gte, $lte, $eq, $ne also available
})

// Array matching
await cosmic.objects.find({
  type: 'products',
  'metadata.tags': { $in: ['sale', 'featured'] }  // Any match
  // $all for all must match, $nin for none match
})

// Logical operators
await cosmic.objects.find({
  type: 'products',
  $and: [
    { 'metadata.price': { $lte: 50 } },
    { 'metadata.in_stock': true }
  ]
  // $or, $not, $nor also available
})

// Text search
await cosmic.objects.find({
  type: 'posts',
  title: { $regex: 'hello', $options: 'i' }  // Case insensitive
})

Query Options

await cosmic.objects.find({ type: 'posts' })
  .props(['title', 'slug', 'metadata'])
  .sort('-created_at')  // Descending by created date
  .limit(10)
  .skip(20)             // Pagination
  .depth(2)             // Resolve nested Object relationships
  .status('any')        // 'published' | 'draft' | 'any'

Media

// List media
const media = await cosmic.media
  .find({ folder: 'images' })
  .props(['url', 'imgix_url', 'alt_text'])
  .limit(20)

// Upload media
const uploaded = await cosmic.media.insertOne({
  media: { originalname: 'photo.jpg', buffer: fileBuffer },
  folder: 'uploads',
  alt_text: 'Description of image',
  metadata: { caption: 'Photo caption' }
})

// Update media
await cosmic.media.updateOne('media-id', {
  alt_text: 'Updated alt text',
  folder: 'new-folder'
})

// Delete media
await cosmic.media.deleteOne('media-id')

imgix Image Processing

All images have imgix_url for transformations:

const optimized = `${media.imgix_url}?w=800&auto=format,compress`
const thumbnail = `${media.imgix_url}?w=100&h=100&fit=crop`

AI Generation

Cosmic provides built-in AI capabilities for text, images, and video.

Generate Text

// Simple prompt
const text = await cosmic.ai.generateText({
  prompt: 'Write a product description for a coffee mug',
  model: 'claude-sonnet-4-5-20250929', // Optional
  max_tokens: 500
})
console.log(text.text)

// Chat messages
const chat = await cosmic.ai.generateText({
  messages: [
    { role: 'user', content: 'Tell me about coffee' },
    { role: 'assistant', content: 'Coffee is a beverage...' },
    { role: 'user', content: 'What about espresso?' }
  ]
})

// Analyze image or document
const analysis = await cosmic.ai.generateText({
  prompt: 'Describe this image',
  media_url: 'https://cdn.cosmicjs.com/image.jpg'
})

// Streaming
const stream = await cosmic.ai.stream({
  prompt: 'Write a blog post',
  max_tokens: 1000
})
for await (const chunk of stream) {
  process.stdout.write(chunk.text || '')
}

Generate Image

const image = await cosmic.ai.generateImage({
  prompt: 'Mountain landscape at sunset',
  model: 'gemini-3-pro-image-preview', // Default, supports up to 4K
  size: '1024x1024',
  folder: 'ai-generated',
  alt_text: 'AI mountain landscape'
})
console.log(image.media.url)

// With reference images (Gemini only)
const styled = await cosmic.ai.generateImage({
  prompt: 'Same style but with ocean',
  reference_images: ['https://cdn.cosmicjs.com/style-ref.jpg']
})

Generate Video

const video = await cosmic.ai.generateVideo({
  prompt: 'A kitten playing with yarn in sunlight',
  model: 'veo-3.1-fast-generate-preview', // Fast: 30-90s generation
  duration: 8,        // 4, 6, or 8 seconds
  resolution: '720p', // or '1080p'
  folder: 'videos'
})
console.log(video.media.url)

// With reference image as first frame
const productVideo = await cosmic.ai.generateVideo({
  prompt: 'Product rotates smoothly',
  reference_images: ['https://cdn.cosmicjs.com/product.jpg'],
  duration: 6
})

// Extend existing video
const extended = await cosmic.ai.extendVideo({
  media_id: video.media.id,
  prompt: 'The kitten walks away into the garden'
})

Available AI Models

Text: claude-sonnet-4-5-20250929 (recommended), claude-opus-4-5-20251101, gpt-5, gemini-3-pro-preview

Image: gemini-3-pro-image-preview (default, up to 4K), dall-e-3

Video: veo-3.1-fast-generate-preview (recommended), veo-3.1-generate-preview (premium)

Framework Patterns

Next.js App Router

// app/posts/page.tsx
import { createBucketClient } from '@cosmicjs/sdk'

const cosmic = createBucketClient({
  bucketSlug: process.env.COSMIC_BUCKET_SLUG!,
  readKey: process.env.COSMIC_READ_KEY!
})

export default async function Posts() {
  const { objects: posts } = await cosmic.objects
    .find({ type: 'posts' })
    .props(['title', 'slug', 'metadata.excerpt'])
    .limit(10)

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Server Actions

'use server'

export async function createPost(formData: FormData) {
  const cosmic = createBucketClient({
    bucketSlug: process.env.COSMIC_BUCKET_SLUG!,
    readKey: process.env.COSMIC_READ_KEY!,
    writeKey: process.env.COSMIC_WRITE_KEY!
  })

  await cosmic.objects.insertOne({
    title: formData.get('title') as string,
    type: 'posts',
    metadata: {
      content: formData.get('content')
    }
  })
}

Common Patterns

Pagination

const page = 1
const limit = 10

const { objects, total } = await cosmic.objects
  .find({ type: 'posts' })
  .skip((page - 1) * limit)
  .limit(limit)

const totalPages = Math.ceil(total / limit)

Draft Preview

const post = await cosmic.objects
  .findOne({ type: 'posts', slug })
  .status('any')  // Include drafts

Localized Content

const post = await cosmic.objects
  .findOne({ type: 'posts', slug, locale: 'es' })

Select and Multi-Select Values

For new content models, prefer select (single selection) and multi-select (multiple selection) over the legacy select-dropdown type.

  • select returns a plain string from the API. No helper needed.
  • multi-select returns a string[] from the API. No helper needed.
  • select-dropdown (deprecated) returns {key: string, value: string} objects, which can cause "Objects are not valid as a React child" errors in JSX.
// select and multi-select: use values directly
<span>{product.metadata?.status}</span>
{product.metadata?.tags?.map(tag => <span key={tag}>{tag}</span>)}

If your project still uses legacy select-dropdown fields, include this helper in your project (e.g., lib/cosmic.ts):

export function getMetafieldValue(field: unknown): string {
  if (field === null || field === undefined) return '';
  if (typeof field === 'string') return field;
  if (typeof field === 'number' || typeof field === 'boolean') return String(field);
  if (typeof field === 'object' && field !== null && 'value' in field) {
    return String((field as { value: unknown }).value);
  }
  if (typeof field === 'object' && field !== null && 'key' in field) {
    return String((field as { key: unknown }).key);
  }
  return '';
}

The function is safe for all types (strings, numbers, booleans pass through unchanged), so wrap legacy select-dropdown metadata values rendered in JSX:

// Wrong: may crash if field is select-dropdown
<span>{product.metadata?.category}</span>

// Correct: always safe for legacy select-dropdown
<span>{getMetafieldValue(product.metadata?.category)}</span>

Key Reminders

  1. Object type is the SLUG - Use type: 'blog-posts', not type: 'Blog Posts'
  2. Media uses name - Reference media by name property, not URL
  3. Relations use id - Reference related Objects by id, not slug
  4. Never expose writeKey - Keep it server-side only
  5. Use props() - Always specify needed properties for performance
  6. imgix for images - Use imgix_url with query params for optimizations
  7. Use select over select-dropdown - New content models should use select (returns plain strings) instead of select-dropdown (returns objects)
  8. Wrap legacy metadata in JSX - Use getMetafieldValue() for select-dropdown metadata values rendered in JSX

Resources