Skip to main content

Sanity CMS Service

Client-side TypeScript service for Sanity CMS operations. All requests proxy through server-side API endpoints to keep write tokens secure. Location: src/services/sanity.ts

Overview

The Sanity service provides functions for:
  • Episodes - CRUD operations for podcast episodes
  • Guests - Guest profile management (via API proxy)
  • Metadata Parsing - Extract metadata from transcript headers
  • Connection Health - Verify Sanity backend is reachable
All requests go through /api/sanity/* endpoints, which handle authentication and token management.

fetchEpisodes()

Retrieve all episodes.
function fetchEpisodes(): Promise<Episode[]>

Returns

interface Episode {
  _id: string
  _type: 'episodeContent'
  _rev: string
  transcript?: string
  prf?: string
  viralHooks?: string
  metadata: {
    title: string
    guestName: string
    guestLinkedinUrl: string
    episodeNumber: number
    topics: string[]
    summary: string
  }
  releaseDate?: string
  socialPosts?: {
    linkedin: string[]
    instagram: string[]
    twitter: string[]
  }
  generatedAssets?: any[]
  assets?: any[]
  videoAssets?: any[]
  visualSuggestions?: any[]
  scheduledPosts?: any[]
  status: 'draft' | 'in_progress' | 'complete'
  isApproved: boolean
  prfApproved: boolean
  prfApprovedAt?: string
  hooksApproved: boolean
  hooksApprovedAt?: string
  linkedinApproved: boolean
  linkedinApprovedAt?: string
  shareToken?: string
  shareCreatedAt?: string
  galleryUuid?: string
  createdAt: string
  updatedAt: string
  guestRef?: {
    _type: 'reference'
    _ref: string
  }
  guest?: {
    _id: string
    name: string
    linkedinUrl: string
    position: string
    company: string
  }
}

Example

import { fetchEpisodes } from '@/services/sanity'

const episodes = await fetchEpisodes()
console.log(`Found ${episodes.length} episodes`)

// Find specific episode
const ep385 = episodes.find(e => e.metadata.episodeNumber === 385)

fetchEpisode()

Retrieve a single episode by ID.
function fetchEpisode(id: string): Promise<Episode | null>

Parameters

id
string
required
Episode ID

Returns

Episode | null  // null if not found

Example

import { fetchEpisode } from '@/services/sanity'

const episode = await fetchEpisode('episode_abc123')

if (episode) {
  console.log('Episode:', episode.metadata.title)
  console.log('Guest:', episode.guest?.name)
} else {
  console.log('Episode not found')
}

createEpisode()

Create a new episode.
function createEpisode(options?: CreateEpisodeOptions): Promise<Episode>

Parameters

interface CreateEpisodeOptions {
  transcript?: string    // Episode transcript
  guestId?: string       // Guest ID to link
}
options.transcript
string
Episode transcript. If provided, metadata is automatically parsed from header.
options.guestId
string
Guest ID. If provided, guest data pre-fills episode metadata.

Returns

Episode  // Newly created episode

Examples

import { createEpisode } from '@/services/sanity'

// Create episode with guest reference
const episode = await createEpisode({
  guestId: 'guest_123'
})

// Guest data automatically populates:
console.log(episode.metadata.guestName)  // "Chris Pacifico"
console.log(episode.metadata.guestLinkedinUrl)  // "https://..."
Automatic Parsing: The API automatically extracts episode number, guest name, and LinkedIn URL from transcript headers.

updateEpisode()

Update episode fields.
function updateEpisode(
  id: string,
  data: Partial<Record<string, unknown>>
): Promise<Episode>

Parameters

id
string
required
Episode ID
data
object
required
Fields to update (only include fields you want to change)

Returns

Episode  // Updated episode

Examples

import { updateEpisode } from '@/services/sanity'

// Update PRF and approval status
await updateEpisode(episodeId, {
  prf: prfContent,
  prfApproved: true,
  prfApprovedAt: new Date().toISOString()
})
Partial Updates: Only fields you provide are updated. Other fields remain unchanged.

deleteEpisode()

Delete an episode.
function deleteEpisode(id: string): Promise<void>

Parameters

id
string
required
Episode ID

Example

import { deleteEpisode } from '@/services/sanity'

if (confirm('Delete this episode?')) {
  await deleteEpisode(episodeId)
  console.log('Episode deleted')
}
Permanent Deletion: This action cannot be undone. The episode and all associated data will be permanently removed.

Metadata Parsing

The service automatically parses metadata from transcript headers:

Supported Formats

// Format 1: Episode number with dash
385-Chris Pacifico

// Format 2: EP prefix
EP 385 - IT Leadership in the Cloud Era

// Guest line
Guest: Chris Pacifico

// LinkedIn URL (anywhere in first 10 lines)
https://www.linkedin.com/in/chris-pacifico/
linkedin.com/in/chris-pacifico  // Also supported

Parsed Fields

{
  episodeNumber: 385,
  guestName: "Chris Pacifico",
  title: "Chris Pacifico",  // From episode number line
  guestLinkedinUrl: "https://www.linkedin.com/in/chris-pacifico/"
}

checkSanityConnection()

Verify Sanity backend is configured and reachable.
function checkSanityConnection(): Promise<{
  connected: boolean
  error?: string
}>

Example

import { checkSanityConnection } from '@/services/sanity'

const { connected, error } = await checkSanityConnection()

if (!connected) {
  console.error('Sanity connection failed:', error)
  showToast('Cannot connect to CMS', 'error')
}
Use this for:
  • Health checks on app startup
  • Debugging connection issues
  • Verifying API token configuration

Guest Functions

Guest management functions are documented in the Guests API page. They follow the same pattern:
// Guest functions (proxy to API)
await createGuest({ name: 'Chris Pacifico', ... })
await updateGuest(guestId, { status: 'recorded' })
await deleteGuest(guestId)

Episode Number Coercion

Episode numbers are automatically coerced to integers:
import { coerceEpisodeNumber } from '@/lib/episodeNumber'

// From string
coerceEpisodeNumber('385')      // 385
coerceEpisodeNumber('EP 385')   // 385
coerceEpisodeNumber('385.5')    // 385 (truncated)

// From number
coerceEpisodeNumber(385)        // 385
coerceEpisodeNumber(385.7)      // 385 (truncated)

// Invalid
coerceEpisodeNumber('abc')      // 0
coerceEpisodeNumber('')         // 0
coerceEpisodeNumber(null)       // 0
This ensures episode numbers are always valid integers in Sanity.

Error Handling

All functions throw errors that should be caught:
try {
  const episodes = await fetchEpisodes()
} catch (error) {
  if (error.message === 'Session expired') {
    // Auto-redirect to login (handled by service)
  } else if (error.message.includes('API error: 401')) {
    // Authentication issue
    console.error('Not authenticated')
  } else {
    // Other error
    console.error('Failed to fetch episodes:', error.message)
  }
}

Auto-Redirect on 401

If the API returns 401 Unauthorized, the service automatically redirects to login:
if (response.status === 401) {
  window.location.href = `/login?returnTo=${encodeURIComponent(window.location.pathname)}`
}
No need to handle this manually in your components.

TypeScript Types

All types are exported for use in your code:
import type { 
  Episode, 
  CreateEpisodeOptions 
} from '@/services/sanity'

function MyComponent() {
  const [episode, setEpisode] = useState<Episode | null>(null)
  
  // ...
}

Build docs developers (and LLMs) love