Skip to content

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 {
getEpisodes,
getEpisode,
getGuests,
getGuest,
getPodcast,
getFeatured
} from '@rejected-media/podcast-framework-core';
---
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);
---
---
import { getEpisodes } from '@rejected-media/podcast-framework-core';
const episodes = await getEpisodes();
---

Result: Same data, 80% less code!

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 first
const 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[];
}

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 });
}
---

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;
}

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
}

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;
}

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 episodes
const featured = await getFeatured();
// Get top 3 featured episodes
const 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
}

Helpers automatically read from environment variables:

.env
PUBLIC_SANITY_PROJECT_ID="abc123"
PUBLIC_SANITY_DATASET="production"

Configuration:

// Reads automatically from import.meta.env
const 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

All helpers use build-time caching to prevent redundant API calls:

// First call: Fetches from Sanity
const episodes1 = await getEpisodes(); // API call
// Second call: Returns cached data
const 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 seconds
src/pages/index.astro
---
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 episodes
const 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>
// Throws helpful error
await getEpisodes();
// → Error: Missing Sanity project ID. Add PUBLIC_SANITY_PROJECT_ID to your .env file
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)
// Get episodes, then sort by title
const episodes = await getEpisodes();
const sortedByTitle = [...episodes].sort((a, b) =>
a.title.localeCompare(b.title)
);
// Get episodes, then filter
const 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;
});
const episodes = await getEpisodes();
const pageSize = 10;
const page = 1;
const paginated = episodes.slice(
(page - 1) * pageSize,
page * pageSize
);
const episodes = await getEpisodes();
// Group by year
const 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, ...],
// }
---
// ✅ Good - Fetch once
const episodes = await getEpisodes();
const featured = episodes.filter(ep => ep.featured);
const recent = episodes.slice(0, 6);
// ❌ Bad - Multiple fetches
const featured = await getEpisodes(); // Fetch 1
const recent = await getEpisodes(); // Fetch 2 (redundant)
---
---
// ✅ Good - Limit at query level
const featured = await getFeatured(3);
// ❌ Less efficient - Fetch all, then slice
const allFeatured = await getFeatured();
const top3 = allFeatured.slice(0, 3);
---
---
// First component: API call
const episodes1 = await getEpisodes();
// Second component: Cached (same build)
const episodes2 = await getEpisodes();
// → No API call, returns cached data
---

Add to .env:

Terminal window
PUBLIC_SANITY_PROJECT_ID="your-project-id"
PUBLIC_SANITY_DATASET="production"

Get project ID:

Terminal window
npx sanity projects list

Check Sanity Studio has content:

  1. Open http://localhost:3333
  2. Create an episode
  3. Click “Publish”
  4. Rebuild site: npm run build

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.

Restart dev server to clear cache:

Terminal window
# Stop server (Ctrl+C)
npm run dev

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 query
const customEpisodes = await client.fetch(`
*[_type == "episode" && _id in $ids] {
title,
slug,
customField
}
`, { ids: ['id1', 'id2'] });