Sanity Helpers
Sanity Helpers
Section titled “Sanity Helpers”Sanity Helpers are convenience functions that automatically create a Sanity client from environment variables. They eliminate boilerplate code and make data fetching simple.
Import
Section titled “Import”import { getEpisodes, getEpisode, getGuests, getGuest, getPodcast, getFeatured} from '@rejected-media/podcast-framework-core';Why Use Helpers?
Section titled “Why Use Helpers?”Without Helpers (Verbose)
Section titled “Without Helpers (Verbose)”---import { createSanityClient, getAllEpisodes } from '@rejected-media/podcast-framework-core';
const sanityClient = createSanityClient({ projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID, dataset: import.meta.env.PUBLIC_SANITY_DATASET, apiVersion: '2024-01-01', useCdn: true});
const episodes = await getAllEpisodes(sanityClient);---With Helpers (Simple)
Section titled “With Helpers (Simple)”---import { getEpisodes } from '@rejected-media/podcast-framework-core';
const episodes = await getEpisodes();---Result: Same data, 80% less code!
Functions
Section titled “Functions”getEpisodes()
Section titled “getEpisodes()”Fetch all episodes with auto-configuration.
Signature:
function getEpisodes(options?: { orderBy?: 'desc' | 'asc'}): Promise<Episode[]>Parameters:
options.orderBy- Sort order by episode number (default:'desc')
Returns: Array of episodes
Examples:
---import { getEpisodes } from '@rejected-media/podcast-framework-core';
// Get all episodes (newest first)const episodes = await getEpisodes();
// Get episodes oldest firstconst episodesOldest = await getEpisodes({ orderBy: 'asc' });---
<h1>All Episodes ({episodes.length})</h1>{episodes.map(episode => ( <article> <h2>Episode {episode.episodeNumber}: {episode.title}</h2> </article>))}Episode Object:
interface Episode { _id: string; title: string; slug: { current: string }; episodeNumber: number; publishDate: string; duration: string; description?: string; showNotes?: any[]; spotifyLink?: string; applePodcastLink?: string; youtubeLink?: string; audioUrl?: string; coverImage?: { url: string }; featured?: boolean; transcript?: string; transcriptSegments?: TranscriptSegment[]; hosts?: Host[]; guests?: Guest[];}getEpisode()
Section titled “getEpisode()”Fetch single episode by slug.
Signature:
function getEpisode(slug: string): Promise<Episode | null>Parameters:
slug- Episode slug
Returns: Episode object or null if not found
Examples:
---import { getEpisode } from '@rejected-media/podcast-framework-core';
const episode = await getEpisode('the-future-of-ethereum');
if (!episode) { return Astro.redirect('/404');}---
<h1>{episode.title}</h1><p>{episode.description}</p>Error Handling:
---const episode = await getEpisode(Astro.params.slug);
if (!episode) { return new Response('Episode not found', { status: 404 });}---getGuests()
Section titled “getGuests()”Fetch all guests alphabetically.
Signature:
function getGuests(): Promise<Guest[]>Returns: Array of guests (sorted by name)
Examples:
---import { getGuests } from '@rejected-media/podcast-framework-core';
const guests = await getGuests();---
<div class="grid grid-cols-4 gap-8"> {guests.map(guest => ( <a href={`/guest/${guest.slug.current}`}> <img src={guest.photo?.url} alt={guest.name} /> <h3>{guest.name}</h3> </a> ))}</div>Guest Object:
interface Guest { _id: string; name: string; slug: { current: string }; bio?: string; photo?: { url: string }; twitter?: string; website?: string; linkedin?: string;}getGuest()
Section titled “getGuest()”Fetch single guest by slug with their episodes.
Signature:
function getGuest(slug: string): Promise<Guest | null>Parameters:
slug- Guest slug
Returns: Guest object with episodes array, or null if not found
Examples:
---import { getGuest } from '@rejected-media/podcast-framework-core';
const guest = await getGuest('vitalik-buterin');
if (!guest) { return Astro.redirect('/404');}---
<article> <img src={guest.photo?.url} alt={guest.name} /> <h1>{guest.name}</h1> <p>{guest.bio}</p>
<!-- Guest's episodes --> <h2>Episodes featuring {guest.name}</h2> {guest.episodes?.map(episode => ( <a href={`/episodes/${episode.slug.current}`}> Episode {episode.episodeNumber}: {episode.title} </a> ))}</article>Guest with Episodes:
interface Guest { // ... standard fields episodes?: Episode[]; // All episodes featuring this guest}getPodcast()
Section titled “getPodcast()”Fetch podcast metadata.
Signature:
function getPodcast(): Promise<PodcastInfo | undefined>Returns: Podcast info or undefined if not found
Examples:
---import { getPodcast } from '@rejected-media/podcast-framework-core';
const podcast = await getPodcast();const siteName = podcast?.name || 'Podcast';---
<title>{siteName}</title><meta name="description" content={podcast?.description} />PodcastInfo Object:
interface PodcastInfo { _id: string; name: string; tagline?: string; description?: string; isActive: boolean; logo?: { url: string }; spotifyShowId?: string; applePodcastsUrl?: string; spotifyUrl?: string; youtubeUrl?: string; rssUrl?: string; twitterUrl?: string; discordUrl?: string; theme?: Theme;}getFeatured()
Section titled “getFeatured()”Fetch featured episodes.
Signature:
function getFeatured(limit?: number): Promise<Episode[]>Parameters:
limit- Maximum number of episodes to return (optional)
Returns: Array of featured episodes (sorted by publish date, newest first)
Examples:
---import { getFeatured } from '@rejected-media/podcast-framework-core';
// Get all featured episodesconst featured = await getFeatured();
// Get top 3 featured episodesconst top3 = await getFeatured(3);---
<FeaturedEpisodesCarousel episodes={featured} />Marking Episodes as Featured:
// In Sanity Studio{ _type: 'episode', title: 'The Future of Ethereum', featured: true // ← Mark as featured}Auto-Configuration
Section titled “Auto-Configuration”Helpers automatically read from environment variables:
PUBLIC_SANITY_PROJECT_ID="abc123"PUBLIC_SANITY_DATASET="production"Configuration:
// Reads automatically from import.meta.envconst projectId = import.meta.env.PUBLIC_SANITY_PROJECT_ID;const dataset = import.meta.env.PUBLIC_SANITY_DATASET || 'production';
// Creates client once (cached globally)const client = createSanityClient({ projectId, dataset });Benefits:
- ✅ No manual client creation
- ✅ Single client instance (performance)
- ✅ Automatic env validation
- ✅ Consistent configuration
Build-Time Caching
Section titled “Build-Time Caching”All helpers use build-time caching to prevent redundant API calls:
// First call: Fetches from Sanityconst episodes1 = await getEpisodes(); // API call
// Second call: Returns cached dataconst episodes2 = await getEpisodes(); // Cache hit (same build)Cache Behavior:
- During build: Cached for 1 minute
- During dev: No caching (fresh data)
- Between builds: Cache cleared
Performance Impact:
Without caching:- 100 pages × 3 queries = 300 API calls- Build time: ~2 minutes
With caching:- Unique queries cached = 5-10 API calls- Build time: ~20 secondsComplete Example
Section titled “Complete Example”---import { getPodcast, getEpisodes, getFeatured} from '@rejected-media/podcast-framework-core';
import BaseLayout from '@rejected-media/podcast-framework-core/layouts/BaseLayout.astro';import FeaturedEpisodesCarousel from '@rejected-media/podcast-framework-core/components/FeaturedEpisodesCarousel.astro';
// Fetch all data (cached automatically)const podcast = await getPodcast();const recentEpisodes = await getEpisodes({ orderBy: 'desc' });const featured = await getFeatured(3);
// Take first 6 episodesconst episodes = recentEpisodes.slice(0, 6);---
<BaseLayout title={podcast?.name} description={podcast?.description}> <!-- Hero --> <section class="hero"> <h1>{podcast?.name}</h1> <p>{podcast?.tagline}</p> </section>
<!-- Featured Episodes --> {featured && featured.length > 0 && ( <FeaturedEpisodesCarousel episodes={featured} /> )}
<!-- Recent Episodes --> <section> <h2>Recent Episodes</h2> <div class="grid grid-cols-3 gap-6"> {episodes.map(episode => ( <article> <a href={`/episodes/${episode.slug.current}`}> <img src={episode.coverImage?.url} alt={episode.title} /> <h3>Episode {episode.episodeNumber}: {episode.title}</h3> </a> </article> ))} </div> </section>
<!-- Active podcast CTA --> {podcast?.isActive && ( <section> <h2>Subscribe to {podcast.name}</h2> <NewsletterSignup /> </section> )}</BaseLayout>Error Handling
Section titled “Error Handling”Missing Project ID
Section titled “Missing Project ID”// Throws helpful errorawait getEpisodes();// → Error: Missing Sanity project ID. Add PUBLIC_SANITY_PROJECT_ID to your .env fileNetwork Errors
Section titled “Network Errors”try { const episodes = await getEpisodes();} catch (error) { console.error('Failed to fetch episodes:', error); // Return empty array or show error message}Helpers handle errors gracefully:
// Returns empty array on error (doesn't throw)const episodes = await getEpisodes();// → [] (if API fails)
// Returns null on error (doesn't throw)const episode = await getEpisode('slug');// → null (if not found or API fails)Advanced Usage
Section titled “Advanced Usage”Custom Sorting
Section titled “Custom Sorting”// Get episodes, then sort by titleconst episodes = await getEpisodes();const sortedByTitle = [...episodes].sort((a, b) => a.title.localeCompare(b.title));Filtering
Section titled “Filtering”// Get episodes, then filterconst episodes = await getEpisodes();
const longEpisodes = episodes.filter(ep => { const seconds = parseDuration(ep.duration); return seconds > 3600; // Over 1 hour});
const recentEpisodes = episodes.filter(ep => { const date = new Date(ep.publishDate); const monthAgo = new Date(); monthAgo.setMonth(monthAgo.getMonth() - 1); return date > monthAgo;});Pagination
Section titled “Pagination”const episodes = await getEpisodes();const pageSize = 10;const page = 1;
const paginated = episodes.slice( (page - 1) * pageSize, page * pageSize);Grouping
Section titled “Grouping”const episodes = await getEpisodes();
// Group by yearconst byYear = episodes.reduce((acc, episode) => { const year = new Date(episode.publishDate).getFullYear(); if (!acc[year]) acc[year] = []; acc[year].push(episode); return acc;}, {} as Record<number, Episode[]>);
// Result:// {// 2024: [episode1, episode2, ...],// 2023: [episode3, episode4, ...],// }Performance Tips
Section titled “Performance Tips”1. Fetch Once, Use Everywhere
Section titled “1. Fetch Once, Use Everywhere”---// ✅ Good - Fetch onceconst episodes = await getEpisodes();const featured = episodes.filter(ep => ep.featured);const recent = episodes.slice(0, 6);
// ❌ Bad - Multiple fetchesconst featured = await getEpisodes(); // Fetch 1const recent = await getEpisodes(); // Fetch 2 (redundant)---2. Use Limit for Featured
Section titled “2. Use Limit for Featured”---// ✅ Good - Limit at query levelconst featured = await getFeatured(3);
// ❌ Less efficient - Fetch all, then sliceconst allFeatured = await getFeatured();const top3 = allFeatured.slice(0, 3);---3. Build-Time Caching is Automatic
Section titled “3. Build-Time Caching is Automatic”---// First component: API callconst episodes1 = await getEpisodes();
// Second component: Cached (same build)const episodes2 = await getEpisodes();// → No API call, returns cached data---Troubleshooting
Section titled “Troubleshooting””Missing Sanity project ID”
Section titled “”Missing Sanity project ID””Add to .env:
PUBLIC_SANITY_PROJECT_ID="your-project-id"PUBLIC_SANITY_DATASET="production"Get project ID:
npx sanity projects listReturns empty array
Section titled “Returns empty array”Check Sanity Studio has content:
- Open http://localhost:3333
- Create an episode
- Click “Publish”
- Rebuild site:
npm run build
Returns undefined or null
Section titled “Returns undefined or null”Check that document exists in Sanity:
const podcast = await getPodcast();
if (!podcast) { console.log('No podcast document found in Sanity');}Create podcast document in Sanity Studio.
Stale data in dev mode
Section titled “Stale data in dev mode”Restart dev server to clear cache:
# Stop server (Ctrl+C)npm run devDirect Sanity Client Access
Section titled “Direct Sanity Client Access”For advanced queries, use low-level functions:
import { createSanityClient, getAllEpisodes} from '@rejected-media/podcast-framework-core';
const client = createSanityClient({ projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID, dataset: import.meta.env.PUBLIC_SANITY_DATASET, apiVersion: '2024-01-01', useCdn: true});
// Custom queryconst customEpisodes = await client.fetch(` *[_type == "episode" && _id in $ids] { title, slug, customField }`, { ids: ['id1', 'id2'] });Related
Section titled “Related”- Sanity Setup - Configure Sanity CMS
- Content Management - Add content
- Static Paths - Generate pages from content
Next Steps
Section titled “Next Steps”- Static Paths - Generate dynamic pages
- Sanity CMS - Set up your CMS
- Components - Use data in components