This package provides YouTube loaders for Astro. It allows you to load YouTube videos using the YouTube Data API v3, and use the data in your Astro site. You can load videos by ID, from channels, through search queries, or from playlists.
The package includes two loaders:
youTubeLoader: Build-time YouTube video loading for build-time content collectionsliveYouTubeLoader: Experimental runtime YouTube video loading for live content collections
npm install @ascorbic/youtube-loaderTo use the YouTube loader, you'll need a YouTube Data API v3 key. Follow these steps to obtain one from the Google Cloud Console:
- Create or Select a Google Cloud Project: If you don't have one, create a new project. Otherwise, select an existing project.
- Enable the YouTube Data API v3: In the Google Cloud Console, navigate to "APIs & Services" > "Library". Search for "YouTube Data API v3" and enable it for your project.
- Create API Credentials: Go to "APIs & Services" > "Credentials". Click "Create Credentials" and choose "API Key".
- Restrict the API Key (Recommended): For security, it's highly recommended to restrict your API key. Click on the newly created API key, then under "API restrictions", select "Restrict key" and choose "YouTube Data API v3" from the dropdown. This ensures the key can only be used for the YouTube API.
Once you have your API key, add it to your .env file in your Astro project:
YOUTUBE_API_KEY="your_youtube_api_key_here"You can use the YouTube loader in your content configuration like this:
// src/content/config.ts
import { defineCollection } from "astro:content";
import { youTubeLoader } from "@ascorbic/youtube-loader";
// Load specific videos by ID
const videos = defineCollection({
loader: youTubeLoader({
type: "videos",
apiKey: import.meta.env.YOUTUBE_API_KEY,
videoIds: ["dQw4w9WgXcQ", "9bZkp7q19f0"],
}),
});
// Load videos from a channel
const channelVideos = defineCollection({
loader: youTubeLoader({
type: "channel",
apiKey: import.meta.env.YOUTUBE_API_KEY,
channelId: "UCuAXFkgsw1L7xaCfnd5JJOw",
maxResults: 50,
order: "date",
}),
});
// Search for videos
const searchResults = defineCollection({
loader: youTubeLoader({
type: "search",
apiKey: import.meta.env.YOUTUBE_API_KEY,
query: "astro framework",
maxResults: 25,
publishedAfter: new Date("2023-01-01"),
}),
});
// Load videos from a playlist
const playlistVideos = defineCollection({
loader: youTubeLoader({
type: "playlist",
apiKey: import.meta.env.YOUTUBE_API_KEY,
playlistId: "PLqGQbXn_GDmnHxd6p_tTlN3d5pMhTjy8g",
maxResults: 50,
}),
});
export const collections = { videos, channelVideos, searchResults, playlistVideos };You can then use these like any other collection in Astro:
---
import { getCollection } from "astro:content";
import Layout from "../layouts/Layout.astro";
const videos = await getCollection("videos");
---
<Layout title="Videos">
<h2>YouTube Videos</h2>
<div class="video-grid">
{
videos.map((video) => (
<div class="video-card">
<a href={video.data.url} target="_blank">
<img src={video.data.thumbnails.medium?.url} alt={video.data.title} />
<h3>{video.data.title}</h3>
<p>{video.data.channelTitle}</p>
<p>{video.data.viewCount} views</p>
</a>
</div>
))
}
</div>
</Layout>When using type: "playlist", you can load all videos from a specific YouTube playlist. This is useful for curated content collections:
const tutorialSeries = defineCollection({
loader: youTubeLoader({
type: "playlist",
apiKey: import.meta.env.YOUTUBE_API_KEY,
playlistId: "PLqGQbXn_GDmnHxd6p_tTlN3d5pMhTjy8g",
maxResults: 100, // Load up to 100 videos from the playlist
}),
});Videos from playlists maintain their playlist order and include all the same metadata as individual videos.
You can render the video description using the render() function:
---
import { render, getEntry } from "astro:content";
const video = await getEntry("videos", Astro.params.id);
const { Content } = await render(video);
---
<h1>{video.data.title}</h1>
<p>By: {video.data.channelTitle}</p>
<p>Published: {video.data.publishedAt.toLocaleDateString()}</p>
<p>Duration: {video.data.duration}</p>
<p>Views: {video.data.viewCount}</p>
<div class="video-embed">
<iframe
src={`https://www.youtube.com/embed/${video.data.id}`}
title={video.data.title}
frameborder="0"
allowfullscreen
></iframe>
</div>
<Content />
⚠️ Experimental Feature: Live content collections require Astro 5.10.0 or later and are currently experimental. The API may change in future versions.
Live YouTube loading allows you to fetch YouTube videos at request time rather than build time. This is useful for displaying fresh data without rebuilding your site.
- Enable live content collections in your
astro.config.mjs:
export default defineConfig({
// ...
experimental: {
liveContentCollections: true,
},
});- Create a live configuration file at
src/live.config.ts:
// src/live.config.ts
import { defineLiveCollection } from "astro:content";
import { liveYouTubeLoader } from "@ascorbic/youtube-loader";
const latestVideos = defineLiveCollection({
type: "live",
loader: liveYouTubeLoader({
type: "channel",
apiKey: import.meta.env.YOUTUBE_API_KEY,
channelId: "UCuAXFkgsw1L7xaCfnd5JJOw",
defaultMaxResults: 10,
}),
});
const searchVideos = defineLiveCollection({
type: "live",
loader: liveYouTubeLoader({
type: "search",
apiKey: import.meta.env.YOUTUBE_API_KEY,
query: "web development",
defaultMaxResults: 25,
}),
});
const playlistVideos = defineLiveCollection({
type: "live",
loader: liveYouTubeLoader({
type: "playlist",
apiKey: import.meta.env.YOUTUBE_API_KEY,
playlistId: "PLqGQbXn_GDmnHxd6p_tTlN3d5pMhTjy8g",
defaultMaxResults: 50,
}),
});
export const collections = { latestVideos, searchVideos, playlistVideos };- Use live collections in your pages:
---
// src/pages/videos/index.astro
import { getLiveCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro';
export const prerender = false; // Required for live content
const { entries: videos, error } = await getLiveCollection('latestVideos', {
limit: 10,
order: 'date'
});
if (error) {
console.error('Failed to load videos:', error.message);
}
---
<Layout title="Latest Videos">
{error ? (
<p>Error loading videos: {error.message}</p>
) : (
<div class="video-grid">
{videos?.map((video) => (
<div class="video-card">
<a href={`/videos/${video.id}`}>
<img src={video.data.thumbnails.medium?.url} alt={video.data.title} />
<h3>{video.data.title}</h3>
<p>{video.data.channelTitle}</p>
<p>{new Date(video.data.publishedAt).toLocaleDateString()}</p>
</a>
</div>
))}
</div>
)}
</Layout>- Create individual video pages with server-side rendering:
---
// src/pages/videos/[id].astro
import { getLiveEntry } from 'astro:content';
import Layout from '../../layouts/Layout.astro';
export const prerender = false; // Required for live content
const { id } = Astro.params;
const { entry: video, error } = await getLiveEntry('latestVideos', id!);
if (error || !video) {
return Astro.redirect('/videos');
}
---
<Layout title={video.data.title}>
<h1>{video.data.title}</h1>
<p>By: {video.data.channelTitle}</p>
<p>Published: {new Date(video.data.publishedAt).toLocaleDateString()}</p>
<div class="video-embed">
<iframe
src={`https://www.youtube.com/embed/${video.data.id}`}
title={video.data.title}
frameborder="0"
allowfullscreen
></iframe>
</div>
<div class="description">
<p>{video.data.description}</p>
</div>
<a href={video.data.url} target="_blank">Watch on YouTube →</a>
</Layout>The liveYouTubeLoader supports various filtering options:
liveYouTubeLoader({
type: "search",
apiKey: process.env.YOUTUBE_API_KEY,
query: "javascript tutorials",
defaultMaxResults: 20,
defaultOrder: "relevance",
requestOptions: {
headers: {
"User-Agent": "My Astro Site",
},
},
});You can filter live collections when fetching them:
// Get latest 5 videos
const { entries } = await getLiveCollection("latestVideos", { limit: 5 });
// Filter by date range
const { entries } = await getLiveCollection("searchVideos", {
publishedAfter: new Date("2024-01-01"),
publishedBefore: new Date("2024-12-31"),
});
// Filter by duration
const { entries } = await getLiveCollection("searchVideos", {
duration: "medium", // short, medium, or long
limit: 10,
});
// Custom search query (for search-type loaders)
const { entries } = await getLiveCollection("searchVideos", {
query: "react hooks tutorial",
order: "date",
});Live YouTube loaders return structured errors that you can handle appropriately:
import {
YouTubeAPIError,
YouTubeValidationError,
YouTubeConfigurationError,
} from "@ascorbic/youtube-loader";
const { entries, error } = await getLiveCollection("latestVideos");
if (error) {
if (error instanceof YouTubeAPIError) {
if (error.isQuotaExceeded) {
console.error("YouTube API quota exceeded");
} else if (error.isInvalidAPIKey) {
console.error("Invalid YouTube API key");
} else {
console.error(`YouTube API error: ${error.message}`);
}
} else if (error instanceof YouTubeValidationError) {
console.error(`Validation error: ${error.message}`);
} else if (error instanceof YouTubeConfigurationError) {
console.error(`Configuration error: ${error.message}`);
}
}Use live loading when:
- You want real-time YouTube data
- Content updates frequently
- You need dynamic search functionality
- You want to avoid rebuilds for new videos
- Building a video discovery interface
Use static loading when:
- You have a fixed set of videos
- Performance is critical (pre-rendered)
- You want build-time optimization
- You need to process video data extensively
Static content collections loader for build-time YouTube video processing.
type(required):'videos' | 'channel' | 'search' | 'playlist'apiKey(required): Your YouTube Data API v3 keyvideoIds: Array of video IDs (required whentypeis'videos')channelId: YouTube channel ID (required whentypeis'channel', orchannelHandlecan be used)channelHandle: YouTube channel handle (alternative tochannelIdfor'channel'type)query: Search query (required whentypeis'search')playlistId: YouTube playlist ID (required whentypeis'playlist')maxResults: Maximum number of results (default: 25). Note: YouTube API limits this to 50 for most endpoints.order: Sort order ('date' | 'rating' | 'relevance' | 'title' | 'videoCount' | 'viewCount'). Applicable for'channel'and'search'types.publishedAfter: Filter videos published after this date. Applicable for'channel'and'search'types.publishedBefore: Filter videos published before this date. Applicable for'channel'and'search'types.regionCode: Region code for localized results. Applicable for'search'type.categoryId: YouTube category ID. Applicable for'channel'and'search'types.duration: Filter by video duration ('short' | 'medium' | 'long'). Applicable for'channel'and'search'types.parts: Additional YouTube API parts to include (e.g.,["snippet", "contentDetails"])requestOptions: Custom fetch optionsfetchFullDetails:boolean(default:false). Iftrue, the loader will make additional API calls to fetchduration,viewCount,likeCount, andcommentCountfor videos fromchannel,search, andplaylisttypes. Iffalse, these properties may beundefinedfor those types, but it will reduce API quota usage.
Live content collections loader for runtime YouTube video processing.
Same as youTubeLoader, plus:
defaultMaxResults: Default maximum results for live queriesdefaultOrder: Default sort order for live queriesdefaultRegionCode: Default region code for live queriesfetchFullDetails:boolean(default:false). Iftrue, the loader will make additional API calls to fetchduration,viewCount,likeCount, andcommentCountfor videos fromchannel,search, andplaylisttypes. Iffalse, these properties may beundefinedfor those types, but it will reduce API quota usage.
These options can be passed to getLiveCollection to filter the results. Filters are applied at the API level where supported, otherwise they are ignored.
limit: Maximum number of results to return.
limit: Maximum number of results to return.channelId: Override the channel ID specified in the loader options.order: Sort order ('date' | 'rating' | 'relevance' | 'title' | 'videoCount' | 'viewCount').publishedAfter: Filter videos published after this date.publishedBefore: Filter videos published before this date.categoryId: Filter by YouTube video category ID.duration: Filter by video duration ('short' | 'medium' | 'long').
limit: Maximum number of results to return.query: Override the search query specified in the loader options.channelId: Limit search results to a specific channel ID.order: Sort order ('date' | 'rating' | 'relevance' | 'title' | 'videoCount' | 'viewCount').publishedAfter: Filter videos published after this date.publishedBefore: Filter videos published before this date.regionCode: Region code for localized results.categoryId: Filter by YouTube video category ID.duration: Filter by video duration ('short' | 'medium' | 'long').
limit: Maximum number of results to return.
Each video entry returned by the loader conforms to the Video type. When fetchFullDetails is false (the default), properties like duration, viewCount, likeCount, and commentCount may be undefined for videos fetched from channel, search, or playlist types.
If fetchFullDetails is set to true, the returned entries will conform to the VideoWithFullDetails type, where these properties are guaranteed to be present.
// Base Video type (when fetchFullDetails is false)
{
id: string;
title: string;
description: string;
url: string;
publishedAt: Date;
duration?: string; // ISO 8601 format (e.g., "PT4M13S")
channelId: string;
channelTitle: string;
thumbnails: {
default?: { url: string; width: number; height: number };
medium?: { url: string; width: number; height: number };
high?: { url: string; width: number; height: number };
standard?: { url: string; width: number; height: number };
maxres?: { url: string; width: number; height: number };
};
tags?: string[];
categoryId?: string;
viewCount?: string;
likeCount?: string;
commentCount?: string;
liveBroadcastContent?: string;
defaultLanguage?: string;
}
// VideoWithFullDetails type (when fetchFullDetails is true)
// All optional fields above (duration, viewCount, etc.) are guaranteed to be present.When using fetchFullDetails: false, you should handle the possibility of undefined properties. TypeScript's type narrowing can help:
import { getCollection } from "astro:content";
import type { Video, VideoWithFullDetails } from "@ascorbic/youtube-loader";
// Example with fetchFullDetails: false (default)
const videos = await getCollection("videos-without-full-details");
videos.map(videoEntry => {
const video = videoEntry.data; // Type: Video
if (video.duration) {
// TypeScript knows video.duration is string here
console.log(`Video duration: ${video.duration}`);
} else {
console.log("Video duration not available.");
}
});
// Example with fetchFullDetails: true
const videosWithFullDetails = await getCollection("videos-with-full-details");
videosWithFullDetails.map(videoEntry => {
const video = videoEntry.data; // Type: VideoWithFullDetails
// TypeScript knows video.duration is string here, no need for check
console.log(`Video duration: ${video.duration}`);
});YouTubeError: Base error classYouTubeAPIError: YouTube API errors (network, quota, authentication)YouTubeValidationError: Data parsing/validation errorsYouTubeConfigurationError: Configuration/setup errors
fetchYouTubeVideos(): Fetch videos by IDsearchYouTubeVideos(): Search for videosfetchChannelVideos(): Fetch videos from a channeltransformYouTubeVideoToVideo(): Transform YouTube API response to internal format
Set your YouTube API key in your .env file:
YOUTUBE_API_KEY=your_youtube_api_key_hereThe YouTube Data API v3 has quota limits:
- Default quota: 10,000 units per day
- Different operations consume different units
- The loader automatically handles caching to minimize API calls
- Consider the quota impact when choosing between live and static loading
Check out the demo site for complete examples of using the YouTube loader in an Astro project.