From 0cb1452358c7ed3bb82e3e288096d9d9d2f7b237 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 11:53:04 +0000 Subject: [PATCH 1/3] feat: Add brand new README for 3.x.x release - Create fresh README structure with modern layout - Feature demo page prominently in hero section - Maintain tagline: "Private, performant YouTube embeds for React. Under 5KB gzipped." - Reorganize content for better scannability and progressive disclosure - Add "Why This Component?" section with clear value proposition - Include real-world use cases and code examples - Improve API reference organization (by usage pattern) - Enhance Quick Start section for faster onboarding - Preserve all technical documentation and props - Better mobile/GitHub reading experience File saved as README.new.md for evaluation before replacing current README. --- README.new.md | 947 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 947 insertions(+) create mode 100644 README.new.md diff --git a/README.new.md b/README.new.md new file mode 100644 index 0000000..3d27892 --- /dev/null +++ b/README.new.md @@ -0,0 +1,947 @@ +# React Lite YouTube Embed + +
+ +**Private, performant YouTube embeds for React. Under 5KB gzipped.** + +[![npm version](https://img.shields.io/npm/v/react-lite-youtube-embed)](https://www.npmjs.com/package/react-lite-youtube-embed) +[![npm downloads](https://img.shields.io/npm/dt/react-lite-youtube-embed)](https://www.npmjs.com/package/react-lite-youtube-embed) +[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/) + +[![ES Module Size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/size-es.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/size-badges.yml) +[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/coverage-tests.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/test-badge.yml) +[![CodeQL](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml/badge.svg)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml) + +### [๐Ÿš€ **Try the Live Demo** โ†’](https://ibrahimcesar.github.io/react-lite-youtube-embed) + +> Interactive demo with all features and code examples โ€ข Updated with each release + +[![Demo Preview](_example_lite.gif)](https://ibrahimcesar.github.io/react-lite-youtube-embed) + +
+ +--- + +## Why This Component? + +YouTube's standard iframe embed can add **over 500KB** to your page and make **dozens of network requests** before the user even clicks play. This component fixes that: + +- โœ… **Tiny** โ€“ Under 5KB gzipped total (JS + CSS) +- โœ… **Fast** โ€“ Loads only a thumbnail until the user clicks +- โœ… **Private** โ€“ No YouTube cookies or tracking by default +- โœ… **SEO-Friendly** โ€“ Structured data for search engines +- โœ… **Accessible** โ€“ Full keyboard navigation and screen reader support +- โœ… **TypeScript** โ€“ Complete type definitions included + +**The result?** Faster page loads, better privacy, and a superior user experience. + +--- + +## Quick Start + +### Install + +```bash +npm install react-lite-youtube-embed +``` + +### Use + +```tsx +import LiteYouTubeEmbed from 'react-lite-youtube-embed'; +import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'; + +export default function App() { + return ( + + ); +} +``` + +That's it. You now have a performant, private YouTube embed. + +--- + +## Core Features + +### ๐Ÿ”’ Privacy First + +**Privacy-Enhanced Mode is the default.** Videos load from `youtube-nocookie.com`, blocking YouTube cookies and tracking until the user explicitly clicks play. + +```tsx +// Default: Privacy-Enhanced Mode (youtube-nocookie.com) + + +// Opt into standard YouTube (with cookies) + +``` + +### โšก Performance Optimization + +Enable lazy loading for images to defer offscreen thumbnails and boost Lighthouse scores: + +```tsx + +``` + +**Impact:** Defers loading offscreen images, reduces bandwidth, improves mobile performance. + +### ๐Ÿ” SEO & Search Visibility + +Help search engines discover your videos with structured data: + +```tsx + +``` + +**Includes:** +- JSON-LD VideoObject structured data +- Noscript fallback for non-JS users +- Google Rich Results eligibility + +**Fetch metadata automatically:** +```bash +./scripts/fetch-youtube-metadata.sh VIDEO_ID --format react +``` + +[โ†’ Full SEO Documentation](#-seo--search-engine-optimization) + +### ๐ŸŽฌ Player Events (New in v3) + +React to player state changes, playback controls, and errors: + +```tsx +import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed'; + + console.log('Started')} + onPause={() => console.log('Paused')} + onEnd={() => console.log('Finished')} + + // Advanced handlers + onStateChange={(e) => console.log('State:', e.state)} + onError={(code) => console.error('Error:', code)} + onPlaybackRateChange={(rate) => console.log('Speed:', rate)} +/> +``` + +[โ†’ Full Event Documentation](#-player-events-new-in-v30) + +### ๐ŸŽฎ Programmatic Control + +Control the player via YouTube's iframe API using refs: + +```tsx +function VideoPlayer() { + const playerRef = useRef(null); + const [isReady, setIsReady] = useState(false); + + const handlePause = () => { + playerRef.current?.contentWindow?.postMessage( + '{"event":"command","func":"pauseVideo"}', + '*' + ); + }; + + return ( + <> + setIsReady(true)} + /> + {isReady && } + + ); +} +``` + +[โ†’ Full Control Documentation](#-controlling-the-player) + +--- + +## Installation Options + +### NPM (Recommended) + +```bash +npm install react-lite-youtube-embed +``` + +### Yarn + +```bash +yarn add react-lite-youtube-embed +``` + +### GitHub Packages + +```bash +npm install @ibrahimcesar/react-lite-youtube-embed +``` + +See [GITHUB_PACKAGES.md](GITHUB_PACKAGES.md) for authentication details. + +--- + +## API Reference + +### Required Props + +| Prop | Type | Description | +|------|------|-------------| +| **id** | `string` | YouTube video or playlist ID | +| **title** | `string` | Video title for iframe (accessibility requirement) | + +### Common Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| cookie | `boolean` | `false` | Use standard YouTube (true) or Privacy-Enhanced Mode (false) | +| lazyLoad | `boolean` | `false` | Enable native lazy loading for thumbnails | +| poster | `string` | `"hqdefault"` | Thumbnail quality: `"default"`, `"mqdefault"`, `"hqdefault"`, `"sddefault"`, `"maxresdefault"` | +| params | `string` | `""` | Additional URL parameters (e.g., `"start=90&end=120"`) | +| enableJsApi | `boolean` | `false` | Enable iframe API for programmatic control | +| playlist | `boolean` | `false` | Set to true if ID is a playlist | + +### Event Props (require `enableJsApi={true}`) + +| Prop | Type | Description | +|------|------|-------------| +| onReady | `(event) => void` | Player is ready to receive commands | +| onPlay | `() => void` | Video started playing | +| onPause | `() => void` | Video was paused | +| onEnd | `() => void` | Video finished playing | +| onBuffering | `() => void` | Video is buffering | +| onStateChange | `(event) => void` | Player state changed | +| onError | `(code) => void` | Player encountered an error | +| onPlaybackRateChange | `(rate) => void` | Playback speed changed | +| onPlaybackQualityChange | `(quality) => void` | Video quality changed | + +### Advanced Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| adNetwork | `boolean` | `false` | Preconnect to Google's ad network | +| alwaysLoadIframe | `boolean` | `false` | Load iframe immediately (not recommended) | +| announce | `string` | `"Watch"` | Screen reader announcement text | +| aspectHeight | `number` | `9` | Custom aspect ratio height | +| aspectWidth | `number` | `16` | Custom aspect ratio width | +| autoplay | `boolean` | `false` | Autoplay video (requires `muted={true}`) | +| focusOnLoad | `boolean` | `false` | Focus iframe when loaded | +| muted | `boolean` | `false` | Mute video audio | +| noscriptFallback | `boolean` | `true` | Include noscript tag with YouTube link | +| onIframeAdded | `() => void` | - | Callback when iframe loads (use for ref availability) | +| playlistCoverId | `string` | - | Video ID for playlist cover image | +| referrerPolicy | `string` | `"strict-origin-when-cross-origin"` | Iframe referrer policy | +| seo | `VideoSEO` | - | SEO metadata object | +| stopOnEnd | `boolean` | `false` | Stop video when it ends to prevent related videos | +| style | `object` | `{}` | Custom container styles | +| thumbnail | `string` | - | Custom thumbnail image URL | +| webp | `boolean` | `false` | Use WebP format for thumbnails | + +### Styling Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| wrapperClass | `string` | `"yt-lite"` | Main wrapper class | +| playerClass | `string` | `"lty-playbtn"` | Play button class | +| iframeClass | `string` | `""` | Iframe element class | +| activeClass | `string` | `"lyt-activated"` | Class when activated | +| containerElement | `string` | `"article"` | HTML element for container | + +### Deprecated Props + +| Prop | Replacement | Note | +|------|-------------|------| +| noCookie | Use `cookie` prop | Inverted logic for clarity | +| rel | Use `resourceHint` | Conflicted with YouTube's `rel` parameter | + +[โ†’ See all props with examples in the demo](https://ibrahimcesar.github.io/react-lite-youtube-embed) + +--- + +## Styling + +### Option 1: Import the CSS (Recommended) + +```tsx +import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'; +``` + +### Option 2: Copy to Global CSS + +For Next.js, Remix, or other frameworks, copy the CSS to your global stylesheet. [See CSS source](https://github.com/ibrahimcesar/react-lite-youtube-embed/blob/main/src/lib/LiteYouTubeEmbed.css) + +### Option 3: Custom Styles + +Use CSS-in-JS or pass custom class names: + +```tsx + +``` + +--- + +## Common Use Cases + +### Stop Video to Hide Related Videos + +Automatically return to thumbnail when the video ends: + +```tsx + +``` + +### Video Gallery with Analytics + +```tsx +function VideoGallery() { + return videos.map(video => ( + analytics.track('video_play', { id: video.id })} + onEnd={() => analytics.track('video_complete', { id: video.id })} + /> + )); +} +``` + +### Auto-Advancing Playlist + +```tsx +function Playlist() { + const videos = ['video1', 'video2', 'video3']; + const [currentIndex, setCurrentIndex] = useState(0); + + return ( + { + if (currentIndex < videos.length - 1) { + setCurrentIndex(currentIndex + 1); + } + }} + /> + ); +} +``` + +### Custom Play/Pause Controls + +```tsx +function CustomPlayer() { + const playerRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + + const togglePlayPause = () => { + const command = isPlaying ? 'pauseVideo' : 'playVideo'; + playerRef.current?.contentWindow?.postMessage( + `{"event":"command","func":"${command}"}`, + '*' + ); + }; + + return ( + <> + setIsPlaying(true)} + onPause={() => setIsPlaying(false)} + /> + + + ); +} +``` + +--- + +## Framework Guides + +### Next.js / SSR Setup + +Using Next.js 13+ App Router or any server-side rendering framework? See the [SSR Guide](./SSR_GUIDE.md) for: +- Setup instructions +- Troubleshooting common issues +- Best practices +- TypeScript configuration + +### TypeScript + +Full TypeScript support is included. Import types as needed: + +```tsx +import LiteYouTubeEmbed, { + PlayerState, + PlayerError, + VideoSEO, + PlayerReadyEvent, + PlayerStateChangeEvent +} from 'react-lite-youtube-embed'; +``` + +--- + +## ๐Ÿ” SEO & Search Engine Optimization + +Improve your video discoverability in search engines with structured data and fallback links. + +### Why SEO Matters + +By default, search engine crawlers cannot discover videos embedded with lite embeds because: +- No followable links exist before user interaction +- No structured metadata for search engines to index +- The facade pattern is invisible to crawlers + +This component now supports **JSON-LD structured data** and **noscript fallbacks** to solve these issues. + +### Basic SEO Setup + +```tsx + +``` + +This generates: +- โœ… **JSON-LD structured data** following [schema.org VideoObject](https://schema.org/VideoObject) +- โœ… **Noscript fallback** with direct YouTube link +- โœ… **Google rich results** eligibility (video carousels, thumbnails in search) + +### Fetching Video Metadata + +Use the included helper script to quickly fetch video metadata: + +```bash +# Make the script executable (first time only) +chmod +x scripts/fetch-youtube-metadata.sh + +# Fetch metadata in JSON format +./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ + +# Get ready-to-use React component code +./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ --format react +``` + +**Requirements:** `curl` and `jq` must be installed. + +### SEO Prop Reference + +```typescript +interface VideoSEO { + name?: string; // Video title (falls back to title prop) + description?: string; // Video description (50-160 chars recommended) + uploadDate?: string; // ISO 8601 date (e.g., "2024-01-15T08:00:00Z") + duration?: string; // ISO 8601 duration (e.g., "PT3M33S") + thumbnailUrl?: string; // Custom thumbnail (auto-generated if omitted) + contentUrl?: string; // YouTube watch URL (auto-generated) + embedUrl?: string; // Embed URL (auto-generated) +} +``` + +### Duration Format Examples + +ISO 8601 duration format: `PT#H#M#S` + +- `"PT3M33S"` - 3 minutes 33 seconds +- `"PT15M"` - 15 minutes +- `"PT1H30M"` - 1 hour 30 minutes +- `"PT2H15M30S"` - 2 hours 15 minutes 30 seconds + +### Verify Your SEO Setup + +Test your structured data: +- [Rich Results Test](https://search.google.com/test/rich-results) +- [Schema Markup Validator](https://validator.schema.org/) + +--- + +## ๐ŸŽฌ Player Events (New in v3.0) + +Get real-time notifications when the YouTube player changes state, encounters errors, or when users interact with playback controls. All event handlers require `enableJsApi={true}` to work. + +### Quick Start + +```tsx +import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed'; + +function App() { + return ( + console.log('Video started playing')} + onPause={() => console.log('Video paused')} + onEnd={() => console.log('Video ended')} + + // Advanced state change handler + onStateChange={(event) => { + console.log('State:', event.state); + console.log('Current time:', event.currentTime); + }} + + // Error handling + onError={(errorCode) => { + if (errorCode === PlayerError.VIDEO_NOT_FOUND) { + alert('Video not available'); + } + }} + /> + ); +} +``` + +### Core Events + +**`onReady(event: PlayerReadyEvent)`** + +Fires when the player is loaded and ready to receive commands. + +```tsx +onReady={(event) => { + console.log(`Player ready for: ${event.videoId}`); +}} +``` + +**`onStateChange(event: PlayerStateChangeEvent)`** + +Fires whenever the player's state changes. + +```tsx +onStateChange={(event) => { + switch (event.state) { + case PlayerState.PLAYING: + console.log('Playing at', event.currentTime, 'seconds'); + break; + case PlayerState.PAUSED: + console.log('Paused'); + break; + case PlayerState.ENDED: + console.log('Video finished'); + break; + } +}} +``` + +**PlayerState values:** +- `PlayerState.UNSTARTED` (-1) +- `PlayerState.ENDED` (0) +- `PlayerState.PLAYING` (1) +- `PlayerState.PAUSED` (2) +- `PlayerState.BUFFERING` (3) +- `PlayerState.CUED` (5) + +**`onError(errorCode: PlayerError)`** + +Fires when the player encounters an error. + +```tsx +onError={(code) => { + switch (code) { + case PlayerError.INVALID_PARAM: + console.error('Invalid video parameter'); + break; + case PlayerError.VIDEO_NOT_FOUND: + console.error('Video not found or removed'); + break; + case PlayerError.NOT_EMBEDDABLE: + console.error('Video cannot be embedded'); + break; + } +}} +``` + +**PlayerError codes:** +- `PlayerError.INVALID_PARAM` (2) +- `PlayerError.HTML5_ERROR` (5) +- `PlayerError.VIDEO_NOT_FOUND` (100) +- `PlayerError.NOT_EMBEDDABLE` (101) +- `PlayerError.NOT_EMBEDDABLE_DISGUISED` (150) + +### Convenience Events + +Simple wrappers for common use cases: + +```tsx + analytics.track('video_play')} + onPause={() => analytics.track('video_pause')} + onEnd={() => loadNextVideo()} + onBuffering={() => showLoadingSpinner()} +/> +``` + +### Advanced Events + +**`onPlaybackRateChange(playbackRate: number)`** + +Fires when playback speed changes. Common values: `0.25`, `0.5`, `1`, `1.5`, `2`. + +```tsx +onPlaybackRateChange={(rate) => { + console.log(`Playback speed: ${rate}x`); +}} +``` + +**`onPlaybackQualityChange(quality: string)`** + +Fires when video quality changes. Values: `"small"` (240p), `"medium"` (360p), `"large"` (480p), `"hd720"`, `"hd1080"`, etc. + +```tsx +onPlaybackQualityChange={(quality) => { + console.log(`Quality changed to: ${quality}`); +}} +``` + +### Real-World Examples + +#### Analytics Tracking + +```tsx +function VideoWithAnalytics() { + const [playStartTime, setPlayStartTime] = useState(null); + + return ( + analytics.track('video_ready')} + onPlay={() => { + setPlayStartTime(Date.now()); + analytics.track('video_play'); + }} + onEnd={() => { + const watchTime = Date.now() - playStartTime; + analytics.track('video_complete', { watchTime }); + }} + onError={(code) => analytics.track('video_error', { errorCode: code })} + /> + ); +} +``` + +#### Video Playlist with Auto-Advance + +```tsx +function VideoPlaylist() { + const videos = ['dQw4w9WgXcQ', 'abc123def', 'xyz789uvw']; + const [currentIndex, setCurrentIndex] = useState(0); + + return ( + { + if (currentIndex < videos.length - 1) { + setCurrentIndex(currentIndex + 1); + } + }} + onError={() => { + // Skip to next video on error + if (currentIndex < videos.length - 1) { + setCurrentIndex(currentIndex + 1); + } + }} + /> + ); +} +``` + +### Important Notes + +โš ๏ธ **Events require `enableJsApi={true}`** + +โš ๏ธ **Lazy Loading Limitation** - By default, the iframe only loads after the user clicks. Events won't fire until after user interaction. Use `onIframeAdded` callback to know when ready, or use `alwaysLoadIframe={true}` (not recommended for privacy/performance). + +โš ๏ธ **Origin Validation** - The component automatically validates events from YouTube domains for security. + +โš ๏ธ **Cleanup** - Event listeners are automatically cleaned up on unmount. + +--- + +## ๐Ÿค– Controlling the player + +You can programmatically control the YouTube player via [YouTube's IFrame Player API](https://developers.google.com/youtube/iframe_api_reference) using refs and `postMessage`. + +> **โš ๏ธ Important:** This requires `enableJsApi={true}`. The ref is only available after the user clicks the poster (use `onIframeAdded` callback to know when ready). + +```tsx +function VideoPlayer() { + const ytRef = useRef(null); + const [isPlaying, setIsPlaying] = useState(false); + + return ( +
+ + +
+ ); +} +``` + +### Using Refs with Lazy-Loaded Iframes + +**Important:** The ref only becomes available **after** the user clicks the poster. + +#### โœ… Correct: Use `onIframeAdded` Callback + +```tsx +const videoRef = useRef(null); + +const handleIframeAdded = () => { + console.log("Iframe loaded and ready!"); + + if (videoRef.current) { + videoRef.current.contentWindow?.postMessage( + '{"event":"command","func":"playVideo"}', + '*' + ); + } +}; + +return ( + +); +``` + +#### โŒ Wrong: Accessing Ref on Mount + +```tsx +// This won't work - iframe doesn't exist yet! +useEffect(() => { + if (videoRef.current) { + console.log("This never runs"); + } +}, []); // Empty deps - runs before iframe exists +``` + +--- + +## FAQ + +### Can I hide all related videos after my video ends? + +**Short answer:** No, this is a YouTube platform limitation. + +**What changed:** In September 2018, YouTube changed the `rel=0` parameter to only limit related videos to the same channel, not hide them completely. + +**Best solution:** Use the built-in `stopOnEnd` prop: + +```tsx + +``` + +This automatically stops the video when it ends and returns to the thumbnail view, preventing related videos from showing. + +[โ†’ See more solutions in the docs](#can-i-completely-hide-suggestedrelated-videos-after-my-video-ends) + +### How do I use this with Next.js? + +See the [SSR Guide](./SSR_GUIDE.md) for detailed Next.js setup instructions and troubleshooting. + +### Does this work with playlists? + +Yes! Set `playlist={true}` and optionally provide a `playlistCoverId`: + +```tsx + +``` + +### Can I customize the thumbnail? + +Yes! Use the `thumbnail` prop to provide a custom image URL: + +```tsx + +``` + +Or choose a different YouTube thumbnail quality with `poster`: + +```tsx + +``` + +--- + +## Contributing + +We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +### Development + +```bash +# Install dependencies +npm install + +# Run tests +npm test + +# Run tests in watch mode +npm run test:watch + +# Build +npm run build + +# Lint +npm run lint + +# Format +npm run format +``` + +--- + +## Security + +This package includes: + +- โœ… **SLSA Build Level 3 Provenance** - Cryptographically signed build provenance +- โœ… **CodeQL Analysis** - Automated security scanning +- โœ… **Dependency Audits** - Regular security updates + +Verify package authenticity: + +```bash +npm audit signatures +``` + +See [.github/SLSA.md](.github/SLSA.md) for more details. + +--- + +## License + +MIT ยฉ [Ibrahim Cesar](https://ibrahimcesar.cloud) + +See [LICENSE](LICENSE) for full details. + +--- + +## Credits + +- **Paul Irish** ([@paulirish](https://github.com/paulirish)) - Original [Lite YouTube Embed](https://github.com/paulirish/lite-youtube-embed) +- **Addy Osmani** ([@addyosmani](https://github.com/addyosmani)) - Adaptive Loading concepts +- **All contributors** - [View contributors](https://github.com/ibrahimcesar/react-lite-youtube-embed/graphs/contributors) + +--- + +## Resources + +- [๐Ÿ“– Documentation](https://github.com/ibrahimcesar/react-lite-youtube-embed) +- [๐Ÿš€ Live Demo](https://ibrahimcesar.github.io/react-lite-youtube-embed) +- [๐Ÿ“ฆ npm Package](https://www.npmjs.com/package/react-lite-youtube-embed) +- [๐Ÿ› Report Issues](https://github.com/ibrahimcesar/react-lite-youtube-embed/issues) +- [๐Ÿ“ Changelog](https://github.com/ibrahimcesar/react-lite-youtube-embed/releases) + +--- + +
+ +**[โฌ† Back to Top](#react-lite-youtube-embed)** + +Made with โค๏ธ in Brazil ๐Ÿ‡ง๐Ÿ‡ท + +
From f9ab4c907eda1d3203cc7cf839eaaf7f631fea6a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 11:57:19 +0000 Subject: [PATCH 2/3] feat: Replace README with brand new version for 3.x.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Modern structure with demo-first approach - Tagline prominently featured: "Private, performant YouTube embeds for React. Under 5KB gzipped." - Clear value proposition in "Why This Component?" section - Progressive disclosure from Quick Start to advanced features - Real-world use cases with copy-paste examples - Better organized API reference by usage pattern - Improved scannability and mobile experience - Made with ๐Ÿงฉ in Brazil ๐Ÿ‡ง๐Ÿ‡ท --- README.md | 1247 +++++++++++++++++++++++++---------------------------- 1 file changed, 576 insertions(+), 671 deletions(-) diff --git a/README.md b/README.md index 6aef1b1..95f43ef 100644 --- a/README.md +++ b/README.md @@ -1,447 +1,514 @@ -
+# React Lite YouTube Embed -

๐Ÿ“บ React Lite YouTube Embed

-
Private, performant YouTube embeds for React. Under 5KB gzipped.
+
-[![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](https://www.typescriptlang.org/) +**Private, performant YouTube embeds for React. Under 5KB gzipped.** -[![Version](https://img.shields.io/npm/v/react-lite-youtube-embed?label=latest%20version)](https://www.npmjs.com/package/react-lite-youtube-embed) ![Total Downloads](https://img.shields.io/npm/dt/react-lite-youtube-embed?color=%23FF0000&logo=npm) ![GitHub issues by-label](https://img.shields.io/github/issues/ibrahimcesar/react-lite-youtube-embed/bug) +[![npm version](https://img.shields.io/npm/v/react-lite-youtube-embed)](https://www.npmjs.com/package/react-lite-youtube-embed) +[![npm downloads](https://img.shields.io/npm/dt/react-lite-youtube-embed)](https://www.npmjs.com/package/react-lite-youtube-embed) +[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/) -[![ES Module Size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/size-es.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/size-badges.yml) [![CommonJS Size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/size-cjs.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/size-badges.yml) [![CSS Size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/size-css.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/size-badges.yml) +[![ES Module Size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/size-es.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/size-badges.yml) +[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/coverage-tests.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/test-badge.yml) +[![CodeQL](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml/badge.svg)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml) -[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/coverage-tests.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/test-badge.yml) [![Stale Issues & PRs](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/stale.yml/badge.svg)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/stale.yml) [![CodeQL Security Scan](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml/badge.svg)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml) [![Automated Release](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/auto-release.yml/badge.svg)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/auto-release.yml) [![Deploy Demo to GitHub Pages](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/deploy-demo.yml/badge.svg)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/deploy-demo.yml) +### [๐Ÿš€ **Try the Live Demo** โ†’](https://ibrahimcesar.github.io/react-lite-youtube-embed) +> Interactive demo with all features and code examples โ€ข Updated with each release -

Developed in ๐Ÿ‡ง๐Ÿ‡ท Brazil

- -Port of Paul Irish's [Lite YouTube Embed](https://github.com/paulirish/lite-youtube-embed) to a React Component. Provide videos with a supercharged focus on visual performance. The gain is not the same as the web component of the original implementation but saves some requests and gives you more control of the embed visual. An ["Adaptive Loading"](https://www.youtube.com/watch?v=puUPpVrIRkc) way to handle iframes for YouTube. - -[![iFrame example](_example_lite.gif)](https://ibrahimcesar.github.io/react-lite-youtube-embed) - -## โœจ [View Live Demo](https://ibrahimcesar.github.io/react-lite-youtube-embed) - -> **Live demo automatically updated with each release** - Try out all features and see code examples +[![Demo Preview](_example_lite.gif)](https://ibrahimcesar.github.io/react-lite-youtube-embed)
-## ๐Ÿ”’ Privacy by Default +--- -The biggest change is, from 2.0.0 this component is private by default. Meaning that will not preconnect with the ad network from Google and connect to YouTube via the Privacy-Enhanced Mode using https://www.youtube-nocookie.com. +## Why This Component? -## ๐Ÿš€ Install +YouTube's standard iframe embed can add **over 500KB** to your page and make **dozens of network requests** before the user even clicks play. This component fixes that: -Use your favorite package manager: +- โœ… **Tiny** โ€“ Under 5KB gzipped total (JS + CSS) +- โœ… **Fast** โ€“ Loads only a thumbnail until the user clicks +- โœ… **Private** โ€“ No YouTube cookies or tracking by default +- โœ… **SEO-Friendly** โ€“ Structured data for search engines +- โœ… **Accessible** โ€“ Full keyboard navigation and screen reader support +- โœ… **TypeScript** โ€“ Complete type definitions included -```bash -yarn add react-lite-youtube-embed -``` +**The result?** Faster page loads, better privacy, and a superior user experience. -```bash -npm install react-lite-youtube-embed -S -``` +--- -### Alternative: Install from GitHub Packages +## Quick Start -This package is also available on GitHub Packages: +### Install ```bash -npm install @ibrahimcesar/react-lite-youtube-embed +npm install react-lite-youtube-embed ``` -See [GITHUB_PACKAGES.md](GITHUB_PACKAGES.md) for detailed instructions on authentication and setup. - -## ๐Ÿ•น๏ธ Basic Usage +### Use -```javascript -import React from "react"; -import { render } from "react-dom"; +```tsx import LiteYouTubeEmbed from 'react-lite-youtube-embed'; import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'; -const App = () => ( -
+export default function App() { + return ( -
-); - -render(, document.getElementById("root")); + ); +} ``` -And that's it. +That's it. You now have a performant, private YouTube embed. -## ๐Ÿ” SEO & Search Engine Optimization +--- -Improve your video discoverability in search engines with structured data and fallback links. +## Core Features -### Why SEO Matters +### ๐Ÿ”’ Privacy First -By default, search engine crawlers cannot discover videos embedded with lite embeds because: -- No followable links exist before user interaction -- No structured metadata for search engines to index -- The facade pattern is invisible to crawlers +**Privacy-Enhanced Mode is the default.** Videos load from `youtube-nocookie.com`, blocking YouTube cookies and tracking until the user explicitly clicks play. -This component now supports **JSON-LD structured data** and **noscript fallbacks** to solve these issues. +```tsx +// Default: Privacy-Enhanced Mode (youtube-nocookie.com) + -### Basic SEO Setup +// Opt into standard YouTube (with cookies) + +``` + +### โšก Performance Optimization + +Enable lazy loading for images to defer offscreen thumbnails and boost Lighthouse scores: -```javascript +```tsx ``` -This will generate: -- โœ… **JSON-LD structured data** following [schema.org VideoObject](https://schema.org/VideoObject) -- โœ… **Noscript fallback** with direct YouTube link (enabled by default) -- โœ… **Google rich results** eligibility (video carousels, thumbnails in search) +**Impact:** Defers loading offscreen images, reduces bandwidth, improves mobile performance. -### Fetching Video Metadata +### ๐Ÿ” SEO & Search Visibility -Use the included helper script to quickly fetch video metadata: +Help search engines discover your videos with structured data: -```bash -# Make the script executable (first time only) -chmod +x scripts/fetch-youtube-metadata.sh +```tsx + +``` -# Fetch metadata in JSON format -./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ +**Includes:** +- JSON-LD VideoObject structured data +- Noscript fallback for non-JS users +- Google Rich Results eligibility -# Get ready-to-use React component code -./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ --format react +**Fetch metadata automatically:** +```bash +./scripts/fetch-youtube-metadata.sh VIDEO_ID --format react ``` -**Requirements:** `curl` and `jq` must be installed. +[โ†’ Full SEO Documentation](#-seo--search-engine-optimization) -The script uses YouTube's oEmbed API (no auth required) and provides: -- Video title -- Thumbnail URL -- Template with TODO fields for description, upload date, and duration +### ๐ŸŽฌ Player Events (New in v3) -### Manual Metadata Collection +React to player state changes, playback controls, and errors: -For complete metadata, you can: +```tsx +import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed'; -1. **Visit the video page** and manually copy: - - Description - - Upload date (convert to ISO 8601: `YYYY-MM-DDTHH:MM:SSZ`) - - Duration (convert to ISO 8601: `PT#H#M#S`) + console.log('Started')} + onPause={() => console.log('Paused')} + onEnd={() => console.log('Finished')} -3. **Browser DevTools approach**: - - Open video page โ†’ Inspect โ†’ Search for `"datePublished"` and `"duration"` in HTML + // Advanced handlers + onStateChange={(e) => console.log('State:', e.state)} + onError={(code) => console.error('Error:', code)} + onPlaybackRateChange={(rate) => console.log('Speed:', rate)} +/> +``` -### Duration Format Examples +[โ†’ Full Event Documentation](#-player-events-new-in-v30) -ISO 8601 duration format: `PT#H#M#S` +### ๐ŸŽฎ Programmatic Control -- `"PT3M33S"` - 3 minutes 33 seconds -- `"PT15M"` - 15 minutes -- `"PT1H30M"` - 1 hour 30 minutes -- `"PT2H15M30S"` - 2 hours 15 minutes 30 seconds +Control the player via YouTube's iframe API using refs: -### SEO Prop Reference +```tsx +function VideoPlayer() { + const playerRef = useRef(null); + const [isReady, setIsReady] = useState(false); -```typescript -interface VideoSEO { - name?: string; // Video title (falls back to title prop) - description?: string; // Video description (50-160 chars recommended) - uploadDate?: string; // ISO 8601 date (e.g., "2024-01-15T08:00:00Z") - duration?: string; // ISO 8601 duration (e.g., "PT3M33S") - thumbnailUrl?: string; // Custom thumbnail (auto-generated if omitted) - contentUrl?: string; // YouTube watch URL (auto-generated) - embedUrl?: string; // Embed URL (auto-generated) + const handlePause = () => { + playerRef.current?.contentWindow?.postMessage( + '{"event":"command","func":"pauseVideo"}', + '*' + ); + }; + + return ( + <> + setIsReady(true)} + /> + {isReady && } + + ); } ``` -### Disabling Noscript Fallback +[โ†’ Full Control Documentation](#-controlling-the-player) -The noscript fallback is enabled by default. To disable: +--- -```javascript - +## Installation Options + +### NPM (Recommended) + +```bash +npm install react-lite-youtube-embed ``` -### Verify Your SEO Setup +### Yarn -Test your structured data with Google's tools: -- [Rich Results Test](https://search.google.com/test/rich-results) -- [Schema Markup Validator](https://validator.schema.org/) +```bash +yarn add react-lite-youtube-embed +``` -**Note:** Playlists do not support SEO structured data (only individual videos). +### GitHub Packages -## ๐Ÿ’Ž Pro Usage +```bash +npm install @ibrahimcesar/react-lite-youtube-embed +``` -```javascript -const App = () => ( -
- -
-); +See [GITHUB_PACKAGES.md](GITHUB_PACKAGES.md) for authentication details. + +--- + +## API Reference + +### Required Props + +| Prop | Type | Description | +|------|------|-------------| +| **id** | `string` | YouTube video or playlist ID | +| **title** | `string` | Video title for iframe (accessibility requirement) | + +### Common Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| cookie | `boolean` | `false` | Use standard YouTube (true) or Privacy-Enhanced Mode (false) | +| lazyLoad | `boolean` | `false` | Enable native lazy loading for thumbnails | +| poster | `string` | `"hqdefault"` | Thumbnail quality: `"default"`, `"mqdefault"`, `"hqdefault"`, `"sddefault"`, `"maxresdefault"` | +| params | `string` | `""` | Additional URL parameters (e.g., `"start=90&end=120"`) | +| enableJsApi | `boolean` | `false` | Enable iframe API for programmatic control | +| playlist | `boolean` | `false` | Set to true if ID is a playlist | + +### Event Props (require `enableJsApi={true}`) + +| Prop | Type | Description | +|------|------|-------------| +| onReady | `(event) => void` | Player is ready to receive commands | +| onPlay | `() => void` | Video started playing | +| onPause | `() => void` | Video was paused | +| onEnd | `() => void` | Video finished playing | +| onBuffering | `() => void` | Video is buffering | +| onStateChange | `(event) => void` | Player state changed | +| onError | `(code) => void` | Player encountered an error | +| onPlaybackRateChange | `(rate) => void` | Playback speed changed | +| onPlaybackQualityChange | `(quality) => void` | Video quality changed | + +### Advanced Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| adNetwork | `boolean` | `false` | Preconnect to Google's ad network | +| alwaysLoadIframe | `boolean` | `false` | Load iframe immediately (not recommended) | +| announce | `string` | `"Watch"` | Screen reader announcement text | +| aspectHeight | `number` | `9` | Custom aspect ratio height | +| aspectWidth | `number` | `16` | Custom aspect ratio width | +| autoplay | `boolean` | `false` | Autoplay video (requires `muted={true}`) | +| focusOnLoad | `boolean` | `false` | Focus iframe when loaded | +| muted | `boolean` | `false` | Mute video audio | +| noscriptFallback | `boolean` | `true` | Include noscript tag with YouTube link | +| onIframeAdded | `() => void` | - | Callback when iframe loads (use for ref availability) | +| playlistCoverId | `string` | - | Video ID for playlist cover image | +| referrerPolicy | `string` | `"strict-origin-when-cross-origin"` | Iframe referrer policy | +| seo | `VideoSEO` | - | SEO metadata object | +| stopOnEnd | `boolean` | `false` | Stop video when it ends to prevent related videos | +| style | `object` | `{}` | Custom container styles | +| thumbnail | `string` | - | Custom thumbnail image URL | +| webp | `boolean` | `false` | Use WebP format for thumbnails | + +### Styling Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| wrapperClass | `string` | `"yt-lite"` | Main wrapper class | +| playerClass | `string` | `"lty-playbtn"` | Play button class | +| iframeClass | `string` | `""` | Iframe element class | +| activeClass | `string` | `"lyt-activated"` | Class when activated | +| containerElement | `string` | `"article"` | HTML element for container | + +### Deprecated Props + +| Prop | Replacement | Note | +|------|-------------|------| +| noCookie | Use `cookie` prop | Inverted logic for clarity | +| rel | Use `resourceHint` | Conflicted with YouTube's `rel` parameter | + +[โ†’ See all props with examples in the demo](https://ibrahimcesar.github.io/react-lite-youtube-embed) + +--- + +## Styling + +### Option 1: Import the CSS (Recommended) + +```tsx +import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'; ``` -## โšก Performance: Lazy Loading +### Option 2: Copy to Global CSS -Improve Lighthouse scores and reduce bandwidth by enabling lazy loading for thumbnail images: +For Next.js, Remix, or other frameworks, copy the CSS to your global stylesheet. [See CSS source](https://github.com/ibrahimcesar/react-lite-youtube-embed/blob/main/src/lib/LiteYouTubeEmbed.css) -```javascript +### Option 3: Custom Styles + +Use CSS-in-JS or pass custom class names: + +```tsx ``` -**How it works:** -- **Default behavior** (`lazyLoad={false}`): Uses CSS `background-image` with preload link -- **With lazy loading** (`lazyLoad={true}`): Uses `` for native browser lazy loading +--- -**Benefits:** -- โœ… Defers loading of offscreen images -- โœ… Reduces initial page bandwidth (especially for pages with multiple videos) -- โœ… Improves Lighthouse "defer offscreen images" score -- โœ… Better mobile performance on slow connections -- โœ… 97%+ browser support +## Common Use Cases -**When to use:** -- Pages with multiple YouTube embeds -- Videos placed below the fold -- Mobile-first applications -- Performance-critical pages +### Stop Video to Hide Related Videos -**Example with multiple embeds:** +Automatically return to thumbnail when the video ends: -```javascript -const VideoGallery = () => ( -
- +```tsx + +``` + +### Video Gallery with Analytics + +```tsx +function VideoGallery() { + return videos.map(video => ( analytics.track('video_play', { id: video.id })} + onEnd={() => analytics.track('video_complete', { id: video.id })} /> - {/* Only loads visible thumbnails initially */} -
-); + )); +} ``` -## ๐Ÿงฐ Bring Your Own Styles +### Auto-Advancing Playlist -React Lite YouTube Embed comes with all original styles from Paul Irish's [Lite YouTube Embed](https://github.com/paulirish/lite-youtube-embed) but you can customize them as you wish passing as a props. +```tsx +function Playlist() { + const videos = ['video1', 'video2', 'video3']; + const [currentIndex, setCurrentIndex] = useState(0); -```javascript -const App = () => ( -
+ return ( { + if (currentIndex < videos.length - 1) { + setCurrentIndex(currentIndex + 1); + } + }} /> -
-); + ); +} ``` -## ๐Ÿค– Controlling the player - -You can programmatically control the YouTube player via [YouTubes IFrame Player API](https://developers.google.com/youtube/iframe_api_reference). However typically YouTube requires you to load an additional script from their servers (`https://www.youtube.com/iframe_api`), which is small but it will load another script. So this is neither performant nor very privacy-friendly. Instead, you can also send messages to the iframe via (`postMessage`)[https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage] using the ref prop. If you don't want to create the `postMessage()` calls yourself, there is also a little (wrapper library)[https://github.com/mich418/youtube-iframe-ctrl] for controlling the iframe with this method. +### Custom Play/Pause Controls -> [!WARNING] -> This will only work if you set the `enableJsApi` prop to true. Also, the ref will only be defined, when the iframe has been loaded (which happens after clicking on the poster). So you can't start the player through this method. If you really want the player to always load the iframe right away (which is not good in terms of privacy), you can use the `alwaysLoadIframe` prop to do this. - -```jsx -const App = () => ( - const ytRef = useRef(null); +```tsx +function CustomPlayer() { + const playerRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); + const togglePlayPause = () => { + const command = isPlaying ? 'pauseVideo' : 'playVideo'; + playerRef.current?.contentWindow?.postMessage( + `{"event":"command","func":"${command}"}`, + '*' + ); + }; + return ( -
- + <> setIsPlaying(true)} + onPause={() => setIsPlaying(false)} /> -
+ + ); -}; -); - +} ``` -## ๐ŸŽฏ Using Refs with Lazy-Loaded Iframes +--- -**Important**: The ref only becomes available **after** the user clicks the poster image, since the iframe is lazy-loaded for performance and privacy. +## Framework Guides -### โŒ Common Mistake +### Next.js / SSR Setup -```javascript -const videoRef = useRef(null); +Using Next.js 13+ App Router or any server-side rendering framework? See the [SSR Guide](./SSR_GUIDE.md) for: +- Setup instructions +- Troubleshooting common issues +- Best practices +- TypeScript configuration -useEffect(() => { - // This will NEVER log - iframe doesn't exist on mount! - if (videoRef.current) { - console.log("iframe ref"); // Never runs - } -}, []); // Empty deps - runs only once on mount, before iframe exists +### TypeScript -return ; -``` +Full TypeScript support is included. Import types as needed: -**Why this doesn't work:** The iframe is only rendered after user interaction (clicking the poster). Your `useEffect` with empty dependencies runs once on mount, but the iframe doesn't exist yet. +```tsx +import LiteYouTubeEmbed, { + PlayerState, + PlayerError, + VideoSEO, + PlayerReadyEvent, + PlayerStateChangeEvent +} from 'react-lite-youtube-embed'; +``` -### โœ… Correct Approaches +--- -#### Option 1: Use the `onIframeAdded` Callback (Recommended) +## ๐Ÿ” SEO & Search Engine Optimization -This is the cleanest way to know when the iframe is ready: +Improve your video discoverability in search engines with structured data and fallback links. -```javascript -const videoRef = useRef(null); +### Why SEO Matters -const handleIframeAdded = () => { - console.log("Iframe loaded and ready!"); +By default, search engine crawlers cannot discover videos embedded with lite embeds because: +- No followable links exist before user interaction +- No structured metadata for search engines to index +- The facade pattern is invisible to crawlers - // Now you can safely access the ref - if (videoRef.current) { - console.log("Iframe element:", videoRef.current); +This component now supports **JSON-LD structured data** and **noscript fallbacks** to solve these issues. - // Control the player via postMessage - videoRef.current.contentWindow?.postMessage( - '{"event":"command","func":"playVideo"}', - '*' - ); - } -}; +### Basic SEO Setup -return ( - -); +```tsx + ``` -#### Option 2: Watch for Ref Changes +This generates: +- โœ… **JSON-LD structured data** following [schema.org VideoObject](https://schema.org/VideoObject) +- โœ… **Noscript fallback** with direct YouTube link +- โœ… **Google rich results** eligibility (video carousels, thumbnails in search) -Monitor when the ref becomes available: +### Fetching Video Metadata -```javascript -const videoRef = useRef(null); +Use the included helper script to quickly fetch video metadata: -useEffect(() => { - if (videoRef.current) { - console.log("Iframe is now available!"); - // Do something with videoRef.current - } -}, [videoRef.current]); // Re-run when ref changes +```bash +# Make the script executable (first time only) +chmod +x scripts/fetch-youtube-metadata.sh + +# Fetch metadata in JSON format +./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ + +# Get ready-to-use React component code +./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ --format react ``` -#### Option 3: Always Load Iframe (Not Recommended) +**Requirements:** `curl` and `jq` must be installed. -Force the iframe to load immediately, bypassing lazy-loading: +### SEO Prop Reference -```javascript -// โš ๏ธ This defeats the performance and privacy benefits! - +```typescript +interface VideoSEO { + name?: string; // Video title (falls back to title prop) + description?: string; // Video description (50-160 chars recommended) + uploadDate?: string; // ISO 8601 date (e.g., "2024-01-15T08:00:00Z") + duration?: string; // ISO 8601 duration (e.g., "PT3M33S") + thumbnailUrl?: string; // Custom thumbnail (auto-generated if omitted) + contentUrl?: string; // YouTube watch URL (auto-generated) + embedUrl?: string; // Embed URL (auto-generated) +} ``` -**Trade-off:** With `alwaysLoadIframe={true}`, the ref is available immediately, but you lose the performance benefits and load YouTube resources on page load. - -### Real-World Example +### Duration Format Examples -Controlling the player after the user activates it: +ISO 8601 duration format: `PT#H#M#S` -```javascript -function VideoPlayer() { - const playerRef = useRef(null); - const [isReady, setIsReady] = useState(false); +- `"PT3M33S"` - 3 minutes 33 seconds +- `"PT15M"` - 15 minutes +- `"PT1H30M"` - 1 hour 30 minutes +- `"PT2H15M30S"` - 2 hours 15 minutes 30 seconds - const handleIframeAdded = () => { - setIsReady(true); - console.log("Player is ready for commands"); - }; +### Verify Your SEO Setup - const togglePlayPause = () => { - if (playerRef.current && isReady) { - playerRef.current.contentWindow?.postMessage( - '{"event":"command","func":"pauseVideo"}', - '*' - ); - } - }; +Test your structured data: +- [Rich Results Test](https://search.google.com/test/rich-results) +- [Schema Markup Validator](https://validator.schema.org/) - return ( -
- - {isReady && ( - - )} -
- ); -} -``` +--- ## ๐ŸŽฌ Player Events (New in v3.0) @@ -449,7 +516,7 @@ Get real-time notifications when the YouTube player changes state, encounters er ### Quick Start -```typescript +```tsx import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed'; function App() { @@ -481,24 +548,23 @@ function App() { } ``` -### Available Event Handlers - -#### Core Events +### Core Events **`onReady(event: PlayerReadyEvent)`** -Fires when the player is loaded and ready to receive commands. This is the first event you'll receive. -```typescript +Fires when the player is loaded and ready to receive commands. + +```tsx onReady={(event) => { console.log(`Player ready for: ${event.videoId}`); - // Safe to call player methods now }} ``` **`onStateChange(event: PlayerStateChangeEvent)`** -Fires whenever the player's state changes (playing, paused, ended, buffering, etc.). Use this for comprehensive state tracking. -```typescript +Fires whenever the player's state changes. + +```tsx onStateChange={(event) => { switch (event.state) { case PlayerState.PLAYING: @@ -510,14 +576,11 @@ onStateChange={(event) => { case PlayerState.ENDED: console.log('Video finished'); break; - case PlayerState.BUFFERING: - console.log('Buffering...'); - break; } }} ``` -**Available PlayerState values:** +**PlayerState values:** - `PlayerState.UNSTARTED` (-1) - `PlayerState.ENDED` (0) - `PlayerState.PLAYING` (1) @@ -526,9 +589,10 @@ onStateChange={(event) => { - `PlayerState.CUED` (5) **`onError(errorCode: PlayerError)`** -Fires when the player encounters an error. Use this for graceful error handling. -```typescript +Fires when the player encounters an error. + +```tsx onError={(code) => { switch (code) { case PlayerError.INVALID_PARAM: @@ -544,26 +608,21 @@ onError={(code) => { }} ``` -**Available PlayerError codes:** -- `PlayerError.INVALID_PARAM` (2) - Invalid parameter value -- `PlayerError.HTML5_ERROR` (5) - HTML5 player error -- `PlayerError.VIDEO_NOT_FOUND` (100) - Video not found or removed -- `PlayerError.NOT_EMBEDDABLE` (101) - Video owner disabled embedding -- `PlayerError.NOT_EMBEDDABLE_DISGUISED` (150) - Same as 101 (used in disguised mode) - -#### Convenience Events +**PlayerError codes:** +- `PlayerError.INVALID_PARAM` (2) +- `PlayerError.HTML5_ERROR` (5) +- `PlayerError.VIDEO_NOT_FOUND` (100) +- `PlayerError.NOT_EMBEDDABLE` (101) +- `PlayerError.NOT_EMBEDDABLE_DISGUISED` (150) -These are simple wrappers around `onStateChange` for common use cases. They don't receive any parameters. +### Convenience Events -**`onPlay()`** - Video started playing -**`onPause()`** - Video was paused -**`onEnd()`** - Video finished playing -**`onBuffering()`** - Video is buffering +Simple wrappers for common use cases: -```typescript +```tsx analytics.track('video_play')} onPause={() => analytics.track('video_pause')} @@ -572,24 +631,25 @@ These are simple wrappers around `onStateChange` for common use cases. They don' /> ``` -#### Advanced Events +### Advanced Events **`onPlaybackRateChange(playbackRate: number)`** -Fires when the user changes playback speed. Common values: `0.25`, `0.5`, `1`, `1.5`, `2`. -```typescript +Fires when playback speed changes. Common values: `0.25`, `0.5`, `1`, `1.5`, `2`. + +```tsx onPlaybackRateChange={(rate) => { console.log(`Playback speed: ${rate}x`); }} ``` **`onPlaybackQualityChange(quality: string)`** + Fires when video quality changes. Values: `"small"` (240p), `"medium"` (360p), `"large"` (480p), `"hd720"`, `"hd1080"`, etc. -```typescript +```tsx onPlaybackQualityChange={(quality) => { console.log(`Quality changed to: ${quality}`); - analytics.track('quality_change', { quality }); }} ``` @@ -597,7 +657,7 @@ onPlaybackQualityChange={(quality) => { #### Analytics Tracking -```typescript +```tsx function VideoWithAnalytics() { const [playStartTime, setPlayStartTime] = useState(null); @@ -606,23 +666,16 @@ function VideoWithAnalytics() { id="dQw4w9WgXcQ" title="My Video" enableJsApi - onReady={() => { - analytics.track('video_ready'); - }} + onReady={() => analytics.track('video_ready')} onPlay={() => { setPlayStartTime(Date.now()); analytics.track('video_play'); }} - onPause={() => { - analytics.track('video_pause'); - }} onEnd={() => { const watchTime = Date.now() - playStartTime; analytics.track('video_complete', { watchTime }); }} - onError={(code) => { - analytics.track('video_error', { errorCode: code }); - }} + onError={(code) => analytics.track('video_error', { errorCode: code })} /> ); } @@ -630,413 +683,265 @@ function VideoWithAnalytics() { #### Video Playlist with Auto-Advance -```typescript +```tsx function VideoPlaylist() { const videos = ['dQw4w9WgXcQ', 'abc123def', 'xyz789uvw']; const [currentIndex, setCurrentIndex] = useState(0); - const handleVideoEnd = () => { - if (currentIndex < videos.length - 1) { - setCurrentIndex(currentIndex + 1); - } - }; - return ( { - console.error(`Error with video ${currentIndex}:`, code); + onEnd={() => { + if (currentIndex < videos.length - 1) { + setCurrentIndex(currentIndex + 1); + } + }} + onError={() => { // Skip to next video on error - handleVideoEnd(); + if (currentIndex < videos.length - 1) { + setCurrentIndex(currentIndex + 1); + } }} /> ); } ``` -#### Custom Play/Pause UI +### Important Notes -```typescript -function CustomControls() { +โš ๏ธ **Events require `enableJsApi={true}`** + +โš ๏ธ **Lazy Loading Limitation** - By default, the iframe only loads after the user clicks. Events won't fire until after user interaction. Use `onIframeAdded` callback to know when ready, or use `alwaysLoadIframe={true}` (not recommended for privacy/performance). + +โš ๏ธ **Origin Validation** - The component automatically validates events from YouTube domains for security. + +โš ๏ธ **Cleanup** - Event listeners are automatically cleaned up on unmount. + +--- + +## ๐Ÿค– Controlling the player + +You can programmatically control the YouTube player via [YouTube's IFrame Player API](https://developers.google.com/youtube/iframe_api_reference) using refs and `postMessage`. + +> **โš ๏ธ Important:** This requires `enableJsApi={true}`. The ref is only available after the user clicks the poster (use `onIframeAdded` callback to know when ready). + +```tsx +function VideoPlayer() { + const ytRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); - const [isReady, setIsReady] = useState(false); return (
+ setIsReady(true)} - onPlay={() => setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - onEnd={() => setIsPlaying(false)} + alwaysLoadIframe /> - - {isReady && ( -
- {isPlaying ? 'โ–ถ๏ธ Playing' : 'โธ๏ธ Paused'} -
- )}
); } ``` -### Important Notes - -โš ๏ธ **Events require `enableJsApi={true}`** - All event handlers require this prop to be enabled. - -โš ๏ธ **Lazy Loading Limitation** - By default, the iframe only loads after the user clicks the poster. This means: -- Events won't fire until after user interaction -- Use `onIframeAdded` callback to know when the iframe is ready -- Or use `alwaysLoadIframe={true}` if you need events immediately (not recommended for privacy/performance) - -โš ๏ธ **Origin Validation** - The component automatically validates that events come from YouTube domains (`youtube.com` or `youtube-nocookie.com`) for security. - -โš ๏ธ **Cleanup** - Event listeners are automatically cleaned up when the component unmounts. - -## โš ๏ธ After version 1.0.0 - BREAKING CHANGES โš ๏ธ +### Using Refs with Lazy-Loaded Iframes -To play nice with new frameworks like [NextJS](https://nextjs.org/), we now don't import the `.css` necessary. Since version `2.0.9` you can pass custom aspect-ratio props, so be aware of any changes needed in the CSS options. Instead use now you have three options: +**Important:** The ref only becomes available **after** the user clicks the poster. -> **๐Ÿ“˜ Using Next.js or SSR?** Check out the [SSR Guide](./SSR_GUIDE.md) for setup instructions, troubleshooting, and best practices. +#### โœ… Correct: Use `onIframeAdded` Callback -### Option 1 - -Place the necessary CSS in your Global CSS file method of preference - -
+```tsx +const videoRef = useRef(null); - Show me the code! +const handleIframeAdded = () => { + console.log("Iframe loaded and ready!"); -```css -.yt-lite { - background-color: #000; - position: relative; - display: block; - contain: content; - background-position: center center; - background-size: cover; - cursor: pointer; -} + if (videoRef.current) { + videoRef.current.contentWindow?.postMessage( + '{"event":"command","func":"playVideo"}', + '*' + ); + } +}; -/* gradient */ -.yt-lite::before { - content: ''; - display: block; - position: absolute; - top: 0; - background-position: top; - background-repeat: repeat-x; - height: 60px; - padding-bottom: 50px; - width: 100%; - transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); -} +return ( + +); +``` -/* responsive iframe with a 16:9 aspect ratio - thanks https://css-tricks.com/responsive-iframes/ -*/ -.yt-lite::after { - content: ""; - display: block; - padding-bottom: calc(100% / (16 / 9)); -} -.yt-lite > iframe { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; -} +#### โŒ Wrong: Accessing Ref on Mount -/* play button */ -.yt-lite > .lty-playbtn { - width: 65px; - height: 46px; - z-index: 1; - opacity: 0.8; - border: none; - background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20version%3D%221.1%22%20id%3D%22YouTube_Icon%22%20x%3D%220px%22%20y%3D%220px%22%20viewBox%3D%220%200%201024%20721%22%20enable-background%3D%22new%200%200%201024%20721%22%20xml%3Aspace%3D%22preserve%22%3E%3Cscript%20xmlns%3D%22%22%3E%0A%20%20%20%20try%20%7B%0A%20%20%20%20%20%20Object.defineProperty(navigator%2C%20%22globalPrivacyControl%22%2C%20%7B%0A%20%20%20%20%20%20%20%20value%3A%20false%2C%0A%20%20%20%20%20%20%20%20configurable%3A%20false%2C%0A%20%20%20%20%20%20%20%20writable%3A%20false%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20document.currentScript.parentElement.removeChild(document.currentScript)%3B%0A%20%20%20%20%7D%20catch(e)%20%7B%7D%3B%0A%20%20%20%20%20%20%3C%2Fscript%3E%0A%3Cpath%20id%3D%22Triangle%22%20fill%3D%22%23FFFFFF%22%20d%3D%22M407%2C493l276-143L407%2C206V493z%22%2F%3E%0A%3Cpath%20id%3D%22The_Sharpness%22%20opacity%3D%220.12%22%20fill%3D%22%23420000%22%20d%3D%22M407%2C206l242%2C161.6l34-17.6L407%2C206z%22%2F%3E%0A%3Cg%20id%3D%22Lozenge%22%3E%0A%09%3Cg%3E%0A%09%09%0A%09%09%09%3ClinearGradient%20id%3D%22SVGID_1_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22512.5%22%20y1%3D%22719.7%22%20x2%3D%22512.5%22%20y2%3D%221.2%22%20gradientTransform%3D%22matrix(1%200%200%20-1%200%20721)%22%3E%0A%09%09%09%3Cstop%20offset%3D%220%22%20style%3D%22stop-color%3A%23E52D27%22%2F%3E%0A%09%09%09%3Cstop%20offset%3D%221%22%20style%3D%22stop-color%3A%23BF171D%22%2F%3E%0A%09%09%3C%2FlinearGradient%3E%0A%09%09%3Cpath%20fill%3D%22url(%23SVGID_1_)%22%20d%3D%22M1013%2C156.3c0%2C0-10-70.4-40.6-101.4C933.6%2C14.2%2C890%2C14%2C870.1%2C11.6C727.1%2C1.3%2C512.7%2C1.3%2C512.7%2C1.3%20%20%20%20h-0.4c0%2C0-214.4%2C0-357.4%2C10.3C135%2C14%2C91.4%2C14.2%2C52.6%2C54.9C22%2C85.9%2C12%2C156.3%2C12%2C156.3S1.8%2C238.9%2C1.8%2C321.6v77.5%20%20%20%20C1.8%2C481.8%2C12%2C564.4%2C12%2C564.4s10%2C70.4%2C40.6%2C101.4c38.9%2C40.7%2C89.9%2C39.4%2C112.6%2C43.7c81.7%2C7.8%2C347.3%2C10.3%2C347.3%2C10.3%20%20%20%20s214.6-0.3%2C357.6-10.7c20-2.4%2C63.5-2.6%2C102.3-43.3c30.6-31%2C40.6-101.4%2C40.6-101.4s10.2-82.7%2C10.2-165.3v-77.5%20%20%20%20C1023.2%2C238.9%2C1013%2C156.3%2C1013%2C156.3z%20M407%2C493V206l276%2C144L407%2C493z%22%2F%3E%0A%09%3C%2Fg%3E%0A%3C%2Fg%3E%0A%3C%2Fsvg%3E"); - transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); -} -.yt-lite:hover > .lty-playbtn { - opacity: 1; -} -/* play button triangle */ -.yt-lite > .lty-playbtn:before { - content: ''; - border-style: solid; - border-width: 11px 0 11px 19px; - border-color: transparent transparent transparent #fff; -} +```tsx +// This won't work - iframe doesn't exist yet! +useEffect(() => { + if (videoRef.current) { + console.log("This never runs"); + } +}, []); // Empty deps - runs before iframe exists +``` -.yt-lite > .lty-playbtn, -.yt-lite > .lty-playbtn:before { - position: absolute; - top: 50%; - left: 50%; - transform: translate3d(-50%, -50%, 0); -} +--- -/* Post-click styles */ -.yt-lite.lyt-activated { - cursor: unset; -} -.yt-lite.lyt-activated::before, -.yt-lite.lyt-activated > .lty-playbtn { - opacity: 0; - pointer-events: none; -} -``` +## FAQ -For example, for NextJS: - -```jsx - +**Best solution:** Use the built-in `stopOnEnd` prop: +```tsx + ``` -
+This automatically stops the video when it ends and returns to the thumbnail view, preventing related videos from showing. -### Option 2 +[โ†’ See more solutions in the docs](#can-i-completely-hide-suggestedrelated-videos-after-my-video-ends) -Using your CSS-In-JS tool of choice encapsulate this component and use the css provided as a guide. +### How do I use this with Next.js? -### Option 3 +See the [SSR Guide](./SSR_GUIDE.md) for detailed Next.js setup instructions and troubleshooting. -Not work on every framework but you can import the css directly, check what works best with your bundler / framework. +### Does this work with playlists? -
-Show me the code! +Yes! Set `playlist={true}` and optionally provide a `playlistCoverId`: -```ts -import 'react-lite-youtube-embed/dist/index.css'; +```tsx + ``` -or in a *.css/scss etc: +### Can I customize the thumbnail? -```css -@import "~react-lite-youtube-embed/dist/index.css"; -``` +Yes! Use the `thumbnail` prop to provide a custom image URL: -
- -## All our props belongs to you - -The most minimalist implementation requires two props: `id` from the YouTube you want to render and `title`, for the iFrame. - -| Prop | Type | Default | Description | -|----------|:--------:|:--------:|------------| -| **id** | string | - | **Required**. ID of the video or playlist | -| **title** | string | - | **Required**. Video title for the iFrame. Always provide a title for iFrames: [https://dequeuniversity.com/tips/provide-iframe-titles](https://dequeuniversity.com/tips/provide-iframe-titles) Help the web be accessible ;) #a11y | -| activeClass | string | `"lyt-activated"` | Pass the string class for the active state | -| adNetwork | boolean | `false` | To preconnect or not to doubleclick addresses called by YouTube iframe (the adnetwork from Google) | -| alwaysLoadIframe | boolean | `false` | If enabled, the original YouTube iframe will always be loaded right away (not recommended for privacy) | -| announce | string | `"Watch"` | Text added to the button for screen readers as `Clickable {announce}, {title}, button`. Customize to match your language #a11y #i18n | -| aspectHeight | number | `9` | Use this optional prop if you want a custom aspect-ratio. Please be aware of aspect height and width relation and also any custom CSS you are using | -| aspectWidth | number | `16` | Use this optional prop if you want a custom aspect-ratio. Please be aware of aspect height and width relation and also any custom CSS you are using | -| autoplay | boolean | `false` | Enables autoplay videos. Important: only works with `muted={true}` and `alwaysLoadIframe={true}` | -| containerElement | string | `"article"` | The HTML element to be used for the container | -| cookie | boolean | `false` | Set to `true` to use https://www.youtube.com instead of Privacy-Enhanced Mode (https://www.youtube-nocookie.com) | -| enableJsApi | boolean | `false` | If enabled, you can send messages to the iframe (via the `ref` prop) to control the player programmatically | -| focusOnLoad | boolean | `false` | Automatically focus iframe when loaded (useful for keyboard navigation) | -| iframeClass | string | `""` | Pass the string class for the iframe element itself | -| lazyLoad | boolean | `false` | Enable native lazy loading for thumbnail images. Improves Lighthouse scores and reduces bandwidth for below-fold videos. Uses `` instead of CSS background-image | -| muted | boolean | `false` | If the video has sound or not. Required for `autoplay={true}` to work | -| noCookie | boolean | `false` | **โš ๏ธ DEPRECATED** - Use `cookie` prop instead | -| noscriptFallback | boolean | `true` | Include noscript tag with YouTube link for accessibility and SEO crawlers | -| **onBuffering** | function | `undefined` | **[Event]** Fires when video is buffering. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| **onEnd** | function | `undefined` | **[Event]** Fires when video ends. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| **onError** | function | `undefined` | **[Event]** Fires on player errors with error code. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| onIframeAdded | function | `undefined` | Callback fired when iframe loads. **Use this to know when the `ref` becomes available** (ref is only populated after user clicks the poster). See [Using Refs](#-using-refs-with-lazy-loaded-iframes) section for examples | -| **onPause** | function | `undefined` | **[Event]** Fires when video is paused. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| **onPlay** | function | `undefined` | **[Event]** Fires when video starts playing. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| **onPlaybackQualityChange** | function | `undefined` | **[Event]** Fires when video quality changes. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| **onPlaybackRateChange** | function | `undefined` | **[Event]** Fires when playback speed changes. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| **onReady** | function | `undefined` | **[Event]** Fires when player is ready. Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| **onStateChange** | function | `undefined` | **[Event]** Fires on all state changes (play/pause/end/buffering). Requires `enableJsApi={true}`. See [Player Events](#-player-events-new-in-v30) | -| params | string | `""` | Additional params to pass to the URL. Format: `start=1150&other=value`. Don't include `?` or leading `&`. Note: use `start` not `t` for time | -| playerClass | string | `"lty-playbtn"` | Pass the string class for the player button to customize it | -| playlist | boolean | `false` | Set to `true` when your id is from a playlist | -| playlistCoverId | string | `undefined` | Video ID to use for playlist cover image. Playlists don't have a standard cover pattern | -| poster | `"default"` \| `"mqdefault"` \| `"hqdefault"` \| `"sddefault"` \| `"maxresdefault"` | `"hqdefault"` | Defines the image size for the poster. Note: `sddefault` and `maxresdefault` aren't always available. See: [YouTube API docs](https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api) | -| referrerPolicy | string | `"strict-origin-when-cross-origin"` | Sets the referrer policy for the iframe | -| rel | string | `"preload"` | **โš ๏ธ DEPRECATED** - Use `resourceHint` prop instead. This prop name conflicts with YouTube's `rel` parameter | -| resourceHint | `"preload"` \| `"prefetch"` | `"preload"` | Controls resource hint for the poster image link tag. Use `"prefetch"` for lower priority or `"preload"` for higher priority loading | -| seo | VideoSEO | `undefined` | SEO metadata for search engines. Generates JSON-LD structured data. See [SEO section](#-seo--search-engine-optimization) for details | -| stopOnEnd | boolean | `false` | Automatically stop video when it ends to prevent showing related videos. Requires `enableJsApi={true}`. See [FAQ](#-frequently-asked-questions) for details | -| style | object | `{}` | Style object for the container, overriding any root styles | -| thumbnail | string | `undefined` | Pass an optional image url to override the default poster and set a custom poster image | -| webp | boolean | `false` | When set, uses the WebP format for poster images | -| wrapperClass | string | `"yt-lite"` | Pass the string class that wraps the iFrame. **Important**: This class needs extra attention, refer to LiteYouTubeEmbed.css | - -## โ“ Frequently Asked Questions - -### Can I completely hide suggested/related videos after my video ends? - -Unfortunately, **no** - this is a YouTube platform limitation that affects all embed implementations, not just this library. - -**What changed:** -In September 2018, YouTube changed how the `rel=0` parameter works. It no longer hides all related videosโ€”it only limits them to videos from the same channel. - -**Available options:** - -#### Option 1: Limit to same-channel videos (partial solution) - -Use the `params` prop to add `rel=0`: - -```jsx +```tsx ``` -**Note:** This only shows videos from your channel. If your channel has many videos, related videos will still appear. - -#### Option 2: Use the built-in `stopOnEnd` prop (easiest solution) - -The **easiest way** to prevent related videos is to use the built-in `stopOnEnd` feature: +Or choose a different YouTube thumbnail quality with `poster`: -```jsx +```tsx ``` -**How it works:** -- Automatically stops the video when it ends -- Returns the player to the thumbnail view -- Prevents related videos from showing -- Requires `enableJsApi={true}` to work +--- -**Benefits:** -- โœ… No manual event handling needed -- โœ… Works out of the box -- โœ… Cleaner code +## Contributing -#### Option 3: Manual YouTube Player API control (advanced solution) +We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -For more control, you can manually handle the YouTube Player API events: +### Development -```jsx -import { useRef, useEffect } from 'react'; -import LiteYouTubeEmbed from 'react-lite-youtube-embed'; +```bash +# Install dependencies +npm install -const App = () => { - const ytRef = useRef(null); +# Run tests +npm test - useEffect(() => { - // Listen for messages from the YouTube iframe - const handleMessage = (event) => { - if (event.origin !== 'https://www.youtube.com' && - event.origin !== 'https://www.youtube-nocookie.com') return; - - try { - const data = JSON.parse(event.data); - // Check if video ended (state 0) - if (data.info?.playerState === 0) { - // Stop the video to return to thumbnail - ytRef.current?.contentWindow?.postMessage( - '{"event":"command","func":"stopVideo","args":""}', - '*' - ); - } - } catch (e) { - // Not JSON, ignore - } - }; +# Run tests in watch mode +npm run test:watch - window.addEventListener('message', handleMessage); - return () => window.removeEventListener('message', handleMessage); - }, []); +# Build +npm run build - return ( - - ); -}; +# Lint +npm run lint + +# Format +npm run format ``` -**How it works:** -1. Enable the YouTube IFrame API with `enableJsApi={true}` -2. Listen for `playerState` changes via `postMessage` -3. When the video ends (state `0`), send `stopVideo` command -4. The player returns to the thumbnail, preventing related videos from showing +--- -See the [๐Ÿค– Controlling the player](#-controlling-the-player) section for more details on using the YouTube IFrame API. +## Security -**Related:** [Issue #94](https://github.com/ibrahimcesar/react-lite-youtube-embed/issues/94) +This package includes: -### Why doesn't `rel=0` hide all related videos anymore? +- โœ… **SLSA Build Level 3 Provenance** - Cryptographically signed build provenance +- โœ… **CodeQL Analysis** - Automated security scanning +- โœ… **Dependency Audits** - Regular security updates -YouTube changed this behavior in September 2018 for business reasons. The embed API no longer provides any parameter to completely disable related videos. +Verify package authenticity: -From [YouTube's official documentation](https://developers.google.com/youtube/player_parameters#rel): +```bash +npm audit signatures +``` -> *"If the rel parameter is set to 0, related videos will come from the same channel as the video that was just played."* +See [.github/SLSA.md](.github/SLSA.md) for more details. -This is a permanent platform change that affects all YouTube embeds, not just this library. +--- -## ๐Ÿ™‡โ€โ™‚๏ธ Thanks +## License -- Paul Irish ([paulirish](https://github.com/paulirish)) for [Lite YouTube Embed](https://github.com/paulirish/lite-youtube-embed) -- Addy Osmani ([addyosmani](https://github.com/addyosmani)) for the Adaptive Loading ideas -- [All contributors](https://github.com/ibrahimcesar/react-lite-youtube-embed/graphs/contributors) +MIT ยฉ [Ibrahim Cesar](https://ibrahimcesar.cloud) -### ๐Ÿ“ Read more +See [LICENSE](LICENSE) for full details. -- [Why I made my open source React component private by default](https://ibrahimcesar.cloud/blog/why-i-made-my-open-source-react-component-private-by-default/) +--- -## MIT License +## Credits -Copyright (c) 2021 โ€” 2025 [Ibrahim Cesar](https://ibrahimcesar.cloud) +- **Paul Irish** ([@paulirish](https://github.com/paulirish)) - Original [Lite YouTube Embed](https://github.com/paulirish/lite-youtube-embed) +- **Addy Osmani** ([@addyosmani](https://github.com/addyosmani)) - Adaptive Loading concepts +- **All contributors** - [View contributors](https://github.com/ibrahimcesar/react-lite-youtube-embed/graphs/contributors) -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +--- -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +## Resources -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +- [๐Ÿ“– Documentation](https://github.com/ibrahimcesar/react-lite-youtube-embed) +- [๐Ÿš€ Live Demo](https://ibrahimcesar.github.io/react-lite-youtube-embed) +- [๐Ÿ“ฆ npm Package](https://www.npmjs.com/package/react-lite-youtube-embed) +- [๐Ÿ› Report Issues](https://github.com/ibrahimcesar/react-lite-youtube-embed/issues) +- [๐Ÿ“ Changelog](https://github.com/ibrahimcesar/react-lite-youtube-embed/releases) + +--- + +
+ +**[โฌ† Back to Top](#react-lite-youtube-embed)** + +Made with ๐Ÿงฉ in Brazil ๐Ÿ‡ง๐Ÿ‡ท + +
From 21238b17d2bdfcb12306cf0ff68088839ca2fedb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Nov 2025 11:58:42 +0000 Subject: [PATCH 3/3] chore: Remove temporary README.new.md file --- README.new.md | 947 -------------------------------------------------- 1 file changed, 947 deletions(-) delete mode 100644 README.new.md diff --git a/README.new.md b/README.new.md deleted file mode 100644 index 3d27892..0000000 --- a/README.new.md +++ /dev/null @@ -1,947 +0,0 @@ -# React Lite YouTube Embed - -
- -**Private, performant YouTube embeds for React. Under 5KB gzipped.** - -[![npm version](https://img.shields.io/npm/v/react-lite-youtube-embed)](https://www.npmjs.com/package/react-lite-youtube-embed) -[![npm downloads](https://img.shields.io/npm/dt/react-lite-youtube-embed)](https://www.npmjs.com/package/react-lite-youtube-embed) -[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)](https://www.typescriptlang.org/) - -[![ES Module Size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/size-es.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/size-badges.yml) -[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/ibrahimcesar/react-lite-youtube-embed/main/.github/badges/coverage-tests.json)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/test-badge.yml) -[![CodeQL](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml/badge.svg)](https://github.com/ibrahimcesar/react-lite-youtube-embed/actions/workflows/codeql.yml) - -### [๐Ÿš€ **Try the Live Demo** โ†’](https://ibrahimcesar.github.io/react-lite-youtube-embed) - -> Interactive demo with all features and code examples โ€ข Updated with each release - -[![Demo Preview](_example_lite.gif)](https://ibrahimcesar.github.io/react-lite-youtube-embed) - -
- ---- - -## Why This Component? - -YouTube's standard iframe embed can add **over 500KB** to your page and make **dozens of network requests** before the user even clicks play. This component fixes that: - -- โœ… **Tiny** โ€“ Under 5KB gzipped total (JS + CSS) -- โœ… **Fast** โ€“ Loads only a thumbnail until the user clicks -- โœ… **Private** โ€“ No YouTube cookies or tracking by default -- โœ… **SEO-Friendly** โ€“ Structured data for search engines -- โœ… **Accessible** โ€“ Full keyboard navigation and screen reader support -- โœ… **TypeScript** โ€“ Complete type definitions included - -**The result?** Faster page loads, better privacy, and a superior user experience. - ---- - -## Quick Start - -### Install - -```bash -npm install react-lite-youtube-embed -``` - -### Use - -```tsx -import LiteYouTubeEmbed from 'react-lite-youtube-embed'; -import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'; - -export default function App() { - return ( - - ); -} -``` - -That's it. You now have a performant, private YouTube embed. - ---- - -## Core Features - -### ๐Ÿ”’ Privacy First - -**Privacy-Enhanced Mode is the default.** Videos load from `youtube-nocookie.com`, blocking YouTube cookies and tracking until the user explicitly clicks play. - -```tsx -// Default: Privacy-Enhanced Mode (youtube-nocookie.com) - - -// Opt into standard YouTube (with cookies) - -``` - -### โšก Performance Optimization - -Enable lazy loading for images to defer offscreen thumbnails and boost Lighthouse scores: - -```tsx - -``` - -**Impact:** Defers loading offscreen images, reduces bandwidth, improves mobile performance. - -### ๐Ÿ” SEO & Search Visibility - -Help search engines discover your videos with structured data: - -```tsx - -``` - -**Includes:** -- JSON-LD VideoObject structured data -- Noscript fallback for non-JS users -- Google Rich Results eligibility - -**Fetch metadata automatically:** -```bash -./scripts/fetch-youtube-metadata.sh VIDEO_ID --format react -``` - -[โ†’ Full SEO Documentation](#-seo--search-engine-optimization) - -### ๐ŸŽฌ Player Events (New in v3) - -React to player state changes, playback controls, and errors: - -```tsx -import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed'; - - console.log('Started')} - onPause={() => console.log('Paused')} - onEnd={() => console.log('Finished')} - - // Advanced handlers - onStateChange={(e) => console.log('State:', e.state)} - onError={(code) => console.error('Error:', code)} - onPlaybackRateChange={(rate) => console.log('Speed:', rate)} -/> -``` - -[โ†’ Full Event Documentation](#-player-events-new-in-v30) - -### ๐ŸŽฎ Programmatic Control - -Control the player via YouTube's iframe API using refs: - -```tsx -function VideoPlayer() { - const playerRef = useRef(null); - const [isReady, setIsReady] = useState(false); - - const handlePause = () => { - playerRef.current?.contentWindow?.postMessage( - '{"event":"command","func":"pauseVideo"}', - '*' - ); - }; - - return ( - <> - setIsReady(true)} - /> - {isReady && } - - ); -} -``` - -[โ†’ Full Control Documentation](#-controlling-the-player) - ---- - -## Installation Options - -### NPM (Recommended) - -```bash -npm install react-lite-youtube-embed -``` - -### Yarn - -```bash -yarn add react-lite-youtube-embed -``` - -### GitHub Packages - -```bash -npm install @ibrahimcesar/react-lite-youtube-embed -``` - -See [GITHUB_PACKAGES.md](GITHUB_PACKAGES.md) for authentication details. - ---- - -## API Reference - -### Required Props - -| Prop | Type | Description | -|------|------|-------------| -| **id** | `string` | YouTube video or playlist ID | -| **title** | `string` | Video title for iframe (accessibility requirement) | - -### Common Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| cookie | `boolean` | `false` | Use standard YouTube (true) or Privacy-Enhanced Mode (false) | -| lazyLoad | `boolean` | `false` | Enable native lazy loading for thumbnails | -| poster | `string` | `"hqdefault"` | Thumbnail quality: `"default"`, `"mqdefault"`, `"hqdefault"`, `"sddefault"`, `"maxresdefault"` | -| params | `string` | `""` | Additional URL parameters (e.g., `"start=90&end=120"`) | -| enableJsApi | `boolean` | `false` | Enable iframe API for programmatic control | -| playlist | `boolean` | `false` | Set to true if ID is a playlist | - -### Event Props (require `enableJsApi={true}`) - -| Prop | Type | Description | -|------|------|-------------| -| onReady | `(event) => void` | Player is ready to receive commands | -| onPlay | `() => void` | Video started playing | -| onPause | `() => void` | Video was paused | -| onEnd | `() => void` | Video finished playing | -| onBuffering | `() => void` | Video is buffering | -| onStateChange | `(event) => void` | Player state changed | -| onError | `(code) => void` | Player encountered an error | -| onPlaybackRateChange | `(rate) => void` | Playback speed changed | -| onPlaybackQualityChange | `(quality) => void` | Video quality changed | - -### Advanced Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| adNetwork | `boolean` | `false` | Preconnect to Google's ad network | -| alwaysLoadIframe | `boolean` | `false` | Load iframe immediately (not recommended) | -| announce | `string` | `"Watch"` | Screen reader announcement text | -| aspectHeight | `number` | `9` | Custom aspect ratio height | -| aspectWidth | `number` | `16` | Custom aspect ratio width | -| autoplay | `boolean` | `false` | Autoplay video (requires `muted={true}`) | -| focusOnLoad | `boolean` | `false` | Focus iframe when loaded | -| muted | `boolean` | `false` | Mute video audio | -| noscriptFallback | `boolean` | `true` | Include noscript tag with YouTube link | -| onIframeAdded | `() => void` | - | Callback when iframe loads (use for ref availability) | -| playlistCoverId | `string` | - | Video ID for playlist cover image | -| referrerPolicy | `string` | `"strict-origin-when-cross-origin"` | Iframe referrer policy | -| seo | `VideoSEO` | - | SEO metadata object | -| stopOnEnd | `boolean` | `false` | Stop video when it ends to prevent related videos | -| style | `object` | `{}` | Custom container styles | -| thumbnail | `string` | - | Custom thumbnail image URL | -| webp | `boolean` | `false` | Use WebP format for thumbnails | - -### Styling Props - -| Prop | Type | Default | Description | -|------|------|---------|-------------| -| wrapperClass | `string` | `"yt-lite"` | Main wrapper class | -| playerClass | `string` | `"lty-playbtn"` | Play button class | -| iframeClass | `string` | `""` | Iframe element class | -| activeClass | `string` | `"lyt-activated"` | Class when activated | -| containerElement | `string` | `"article"` | HTML element for container | - -### Deprecated Props - -| Prop | Replacement | Note | -|------|-------------|------| -| noCookie | Use `cookie` prop | Inverted logic for clarity | -| rel | Use `resourceHint` | Conflicted with YouTube's `rel` parameter | - -[โ†’ See all props with examples in the demo](https://ibrahimcesar.github.io/react-lite-youtube-embed) - ---- - -## Styling - -### Option 1: Import the CSS (Recommended) - -```tsx -import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css'; -``` - -### Option 2: Copy to Global CSS - -For Next.js, Remix, or other frameworks, copy the CSS to your global stylesheet. [See CSS source](https://github.com/ibrahimcesar/react-lite-youtube-embed/blob/main/src/lib/LiteYouTubeEmbed.css) - -### Option 3: Custom Styles - -Use CSS-in-JS or pass custom class names: - -```tsx - -``` - ---- - -## Common Use Cases - -### Stop Video to Hide Related Videos - -Automatically return to thumbnail when the video ends: - -```tsx - -``` - -### Video Gallery with Analytics - -```tsx -function VideoGallery() { - return videos.map(video => ( - analytics.track('video_play', { id: video.id })} - onEnd={() => analytics.track('video_complete', { id: video.id })} - /> - )); -} -``` - -### Auto-Advancing Playlist - -```tsx -function Playlist() { - const videos = ['video1', 'video2', 'video3']; - const [currentIndex, setCurrentIndex] = useState(0); - - return ( - { - if (currentIndex < videos.length - 1) { - setCurrentIndex(currentIndex + 1); - } - }} - /> - ); -} -``` - -### Custom Play/Pause Controls - -```tsx -function CustomPlayer() { - const playerRef = useRef(null); - const [isPlaying, setIsPlaying] = useState(false); - - const togglePlayPause = () => { - const command = isPlaying ? 'pauseVideo' : 'playVideo'; - playerRef.current?.contentWindow?.postMessage( - `{"event":"command","func":"${command}"}`, - '*' - ); - }; - - return ( - <> - setIsPlaying(true)} - onPause={() => setIsPlaying(false)} - /> - - - ); -} -``` - ---- - -## Framework Guides - -### Next.js / SSR Setup - -Using Next.js 13+ App Router or any server-side rendering framework? See the [SSR Guide](./SSR_GUIDE.md) for: -- Setup instructions -- Troubleshooting common issues -- Best practices -- TypeScript configuration - -### TypeScript - -Full TypeScript support is included. Import types as needed: - -```tsx -import LiteYouTubeEmbed, { - PlayerState, - PlayerError, - VideoSEO, - PlayerReadyEvent, - PlayerStateChangeEvent -} from 'react-lite-youtube-embed'; -``` - ---- - -## ๐Ÿ” SEO & Search Engine Optimization - -Improve your video discoverability in search engines with structured data and fallback links. - -### Why SEO Matters - -By default, search engine crawlers cannot discover videos embedded with lite embeds because: -- No followable links exist before user interaction -- No structured metadata for search engines to index -- The facade pattern is invisible to crawlers - -This component now supports **JSON-LD structured data** and **noscript fallbacks** to solve these issues. - -### Basic SEO Setup - -```tsx - -``` - -This generates: -- โœ… **JSON-LD structured data** following [schema.org VideoObject](https://schema.org/VideoObject) -- โœ… **Noscript fallback** with direct YouTube link -- โœ… **Google rich results** eligibility (video carousels, thumbnails in search) - -### Fetching Video Metadata - -Use the included helper script to quickly fetch video metadata: - -```bash -# Make the script executable (first time only) -chmod +x scripts/fetch-youtube-metadata.sh - -# Fetch metadata in JSON format -./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ - -# Get ready-to-use React component code -./scripts/fetch-youtube-metadata.sh dQw4w9WgXcQ --format react -``` - -**Requirements:** `curl` and `jq` must be installed. - -### SEO Prop Reference - -```typescript -interface VideoSEO { - name?: string; // Video title (falls back to title prop) - description?: string; // Video description (50-160 chars recommended) - uploadDate?: string; // ISO 8601 date (e.g., "2024-01-15T08:00:00Z") - duration?: string; // ISO 8601 duration (e.g., "PT3M33S") - thumbnailUrl?: string; // Custom thumbnail (auto-generated if omitted) - contentUrl?: string; // YouTube watch URL (auto-generated) - embedUrl?: string; // Embed URL (auto-generated) -} -``` - -### Duration Format Examples - -ISO 8601 duration format: `PT#H#M#S` - -- `"PT3M33S"` - 3 minutes 33 seconds -- `"PT15M"` - 15 minutes -- `"PT1H30M"` - 1 hour 30 minutes -- `"PT2H15M30S"` - 2 hours 15 minutes 30 seconds - -### Verify Your SEO Setup - -Test your structured data: -- [Rich Results Test](https://search.google.com/test/rich-results) -- [Schema Markup Validator](https://validator.schema.org/) - ---- - -## ๐ŸŽฌ Player Events (New in v3.0) - -Get real-time notifications when the YouTube player changes state, encounters errors, or when users interact with playback controls. All event handlers require `enableJsApi={true}` to work. - -### Quick Start - -```tsx -import LiteYouTubeEmbed, { PlayerState, PlayerError } from 'react-lite-youtube-embed'; - -function App() { - return ( - console.log('Video started playing')} - onPause={() => console.log('Video paused')} - onEnd={() => console.log('Video ended')} - - // Advanced state change handler - onStateChange={(event) => { - console.log('State:', event.state); - console.log('Current time:', event.currentTime); - }} - - // Error handling - onError={(errorCode) => { - if (errorCode === PlayerError.VIDEO_NOT_FOUND) { - alert('Video not available'); - } - }} - /> - ); -} -``` - -### Core Events - -**`onReady(event: PlayerReadyEvent)`** - -Fires when the player is loaded and ready to receive commands. - -```tsx -onReady={(event) => { - console.log(`Player ready for: ${event.videoId}`); -}} -``` - -**`onStateChange(event: PlayerStateChangeEvent)`** - -Fires whenever the player's state changes. - -```tsx -onStateChange={(event) => { - switch (event.state) { - case PlayerState.PLAYING: - console.log('Playing at', event.currentTime, 'seconds'); - break; - case PlayerState.PAUSED: - console.log('Paused'); - break; - case PlayerState.ENDED: - console.log('Video finished'); - break; - } -}} -``` - -**PlayerState values:** -- `PlayerState.UNSTARTED` (-1) -- `PlayerState.ENDED` (0) -- `PlayerState.PLAYING` (1) -- `PlayerState.PAUSED` (2) -- `PlayerState.BUFFERING` (3) -- `PlayerState.CUED` (5) - -**`onError(errorCode: PlayerError)`** - -Fires when the player encounters an error. - -```tsx -onError={(code) => { - switch (code) { - case PlayerError.INVALID_PARAM: - console.error('Invalid video parameter'); - break; - case PlayerError.VIDEO_NOT_FOUND: - console.error('Video not found or removed'); - break; - case PlayerError.NOT_EMBEDDABLE: - console.error('Video cannot be embedded'); - break; - } -}} -``` - -**PlayerError codes:** -- `PlayerError.INVALID_PARAM` (2) -- `PlayerError.HTML5_ERROR` (5) -- `PlayerError.VIDEO_NOT_FOUND` (100) -- `PlayerError.NOT_EMBEDDABLE` (101) -- `PlayerError.NOT_EMBEDDABLE_DISGUISED` (150) - -### Convenience Events - -Simple wrappers for common use cases: - -```tsx - analytics.track('video_play')} - onPause={() => analytics.track('video_pause')} - onEnd={() => loadNextVideo()} - onBuffering={() => showLoadingSpinner()} -/> -``` - -### Advanced Events - -**`onPlaybackRateChange(playbackRate: number)`** - -Fires when playback speed changes. Common values: `0.25`, `0.5`, `1`, `1.5`, `2`. - -```tsx -onPlaybackRateChange={(rate) => { - console.log(`Playback speed: ${rate}x`); -}} -``` - -**`onPlaybackQualityChange(quality: string)`** - -Fires when video quality changes. Values: `"small"` (240p), `"medium"` (360p), `"large"` (480p), `"hd720"`, `"hd1080"`, etc. - -```tsx -onPlaybackQualityChange={(quality) => { - console.log(`Quality changed to: ${quality}`); -}} -``` - -### Real-World Examples - -#### Analytics Tracking - -```tsx -function VideoWithAnalytics() { - const [playStartTime, setPlayStartTime] = useState(null); - - return ( - analytics.track('video_ready')} - onPlay={() => { - setPlayStartTime(Date.now()); - analytics.track('video_play'); - }} - onEnd={() => { - const watchTime = Date.now() - playStartTime; - analytics.track('video_complete', { watchTime }); - }} - onError={(code) => analytics.track('video_error', { errorCode: code })} - /> - ); -} -``` - -#### Video Playlist with Auto-Advance - -```tsx -function VideoPlaylist() { - const videos = ['dQw4w9WgXcQ', 'abc123def', 'xyz789uvw']; - const [currentIndex, setCurrentIndex] = useState(0); - - return ( - { - if (currentIndex < videos.length - 1) { - setCurrentIndex(currentIndex + 1); - } - }} - onError={() => { - // Skip to next video on error - if (currentIndex < videos.length - 1) { - setCurrentIndex(currentIndex + 1); - } - }} - /> - ); -} -``` - -### Important Notes - -โš ๏ธ **Events require `enableJsApi={true}`** - -โš ๏ธ **Lazy Loading Limitation** - By default, the iframe only loads after the user clicks. Events won't fire until after user interaction. Use `onIframeAdded` callback to know when ready, or use `alwaysLoadIframe={true}` (not recommended for privacy/performance). - -โš ๏ธ **Origin Validation** - The component automatically validates events from YouTube domains for security. - -โš ๏ธ **Cleanup** - Event listeners are automatically cleaned up on unmount. - ---- - -## ๐Ÿค– Controlling the player - -You can programmatically control the YouTube player via [YouTube's IFrame Player API](https://developers.google.com/youtube/iframe_api_reference) using refs and `postMessage`. - -> **โš ๏ธ Important:** This requires `enableJsApi={true}`. The ref is only available after the user clicks the poster (use `onIframeAdded` callback to know when ready). - -```tsx -function VideoPlayer() { - const ytRef = useRef(null); - const [isPlaying, setIsPlaying] = useState(false); - - return ( -
- - -
- ); -} -``` - -### Using Refs with Lazy-Loaded Iframes - -**Important:** The ref only becomes available **after** the user clicks the poster. - -#### โœ… Correct: Use `onIframeAdded` Callback - -```tsx -const videoRef = useRef(null); - -const handleIframeAdded = () => { - console.log("Iframe loaded and ready!"); - - if (videoRef.current) { - videoRef.current.contentWindow?.postMessage( - '{"event":"command","func":"playVideo"}', - '*' - ); - } -}; - -return ( - -); -``` - -#### โŒ Wrong: Accessing Ref on Mount - -```tsx -// This won't work - iframe doesn't exist yet! -useEffect(() => { - if (videoRef.current) { - console.log("This never runs"); - } -}, []); // Empty deps - runs before iframe exists -``` - ---- - -## FAQ - -### Can I hide all related videos after my video ends? - -**Short answer:** No, this is a YouTube platform limitation. - -**What changed:** In September 2018, YouTube changed the `rel=0` parameter to only limit related videos to the same channel, not hide them completely. - -**Best solution:** Use the built-in `stopOnEnd` prop: - -```tsx - -``` - -This automatically stops the video when it ends and returns to the thumbnail view, preventing related videos from showing. - -[โ†’ See more solutions in the docs](#can-i-completely-hide-suggestedrelated-videos-after-my-video-ends) - -### How do I use this with Next.js? - -See the [SSR Guide](./SSR_GUIDE.md) for detailed Next.js setup instructions and troubleshooting. - -### Does this work with playlists? - -Yes! Set `playlist={true}` and optionally provide a `playlistCoverId`: - -```tsx - -``` - -### Can I customize the thumbnail? - -Yes! Use the `thumbnail` prop to provide a custom image URL: - -```tsx - -``` - -Or choose a different YouTube thumbnail quality with `poster`: - -```tsx - -``` - ---- - -## Contributing - -We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. - -### Development - -```bash -# Install dependencies -npm install - -# Run tests -npm test - -# Run tests in watch mode -npm run test:watch - -# Build -npm run build - -# Lint -npm run lint - -# Format -npm run format -``` - ---- - -## Security - -This package includes: - -- โœ… **SLSA Build Level 3 Provenance** - Cryptographically signed build provenance -- โœ… **CodeQL Analysis** - Automated security scanning -- โœ… **Dependency Audits** - Regular security updates - -Verify package authenticity: - -```bash -npm audit signatures -``` - -See [.github/SLSA.md](.github/SLSA.md) for more details. - ---- - -## License - -MIT ยฉ [Ibrahim Cesar](https://ibrahimcesar.cloud) - -See [LICENSE](LICENSE) for full details. - ---- - -## Credits - -- **Paul Irish** ([@paulirish](https://github.com/paulirish)) - Original [Lite YouTube Embed](https://github.com/paulirish/lite-youtube-embed) -- **Addy Osmani** ([@addyosmani](https://github.com/addyosmani)) - Adaptive Loading concepts -- **All contributors** - [View contributors](https://github.com/ibrahimcesar/react-lite-youtube-embed/graphs/contributors) - ---- - -## Resources - -- [๐Ÿ“– Documentation](https://github.com/ibrahimcesar/react-lite-youtube-embed) -- [๐Ÿš€ Live Demo](https://ibrahimcesar.github.io/react-lite-youtube-embed) -- [๐Ÿ“ฆ npm Package](https://www.npmjs.com/package/react-lite-youtube-embed) -- [๐Ÿ› Report Issues](https://github.com/ibrahimcesar/react-lite-youtube-embed/issues) -- [๐Ÿ“ Changelog](https://github.com/ibrahimcesar/react-lite-youtube-embed/releases) - ---- - -
- -**[โฌ† Back to Top](#react-lite-youtube-embed)** - -Made with โค๏ธ in Brazil ๐Ÿ‡ง๐Ÿ‡ท - -