Skip to content

Commit 5050581

Browse files
author
strausr
committed
docs(rules): video player imperative-only, install latest, shorten section
- Video player: single pattern only (imperative element, no React-managed ref) - One example: createElement, append to container ref, pass to videoPlayer; cleanup dispose + removeChild guard - Consolidate common errors (Invalid target, removeChild, CSP) into shorter entries - Install Cloudinary packages: explicit 'install latest' rule, no version pin - Project setup and checklist updated to imperative + fallback to AdvancedVideo
1 parent 8625996 commit 5050581

1 file changed

Lines changed: 63 additions & 62 deletions

File tree

templates/.cursorrules.template

Lines changed: 63 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ export const uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || '';
5555
- **Signed uploads**: Do not use only `uploadPreset`; use the pattern under "Secure (Signed) Uploads" (uploadSignature as function, fetch api_key, server includes upload_preset in signature).
5656

5757
**4. Video player**
58-
- Use `cloudName` from `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` in player config. Validate it before init (e.g. if !cloudName, set error state). See "Cloudinary Video Player (The Player)" for full pattern (named import `videoPlayer`, `player.source({ publicId })`, refs, cleanup).
58+
- Use imperative video element only (create with document.createElement, append to container ref, pass to videoPlayer). See "Cloudinary Video Player (The Player)" for the full pattern.
5959

6060
**5. Summary for rules-only users**
6161
- **Env**: Use your bundler's client env prefix and access (Vite: `VITE_` + `import.meta.env.VITE_*`; see "Other bundlers" if not Vite).
6262
- **Reusable instance**: One config file that creates and exports `cld` (and optionally `uploadPreset`) from `@cloudinary/url-gen`; use it everywhere.
6363
- **Upload widget**: Script in index.html (or equivalent); create widget once in useEffect with ref; unsigned = cloudName + uploadPreset; signed = use uploadSignature function and backend.
64-
- **Video player**: Named import `videoPlayer`, source object, refs, dispose in cleanup.
64+
- **Video player**: Imperative video element (createElement, append to container ref, pass to videoPlayer); dispose + removeChild in cleanup; fall back to AdvancedVideo if init fails.
6565

6666
**If the user is not using Vite:** Use their bundler's client env prefix and access in the config file and everywhere you read env. Examples: Create React App → `REACT_APP_CLOUDINARY_CLOUD_NAME`, `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`; Next.js (client) → `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`, `process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`. The rest (cld instance, widget options, video player) is the same.
6767

@@ -89,6 +89,12 @@ export const uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || '';
8989
3. Is the preset set to **Unsigned** (not Signed)?
9090
4. Was the dev server restarted after adding/updating `.env`?
9191

92+
## Installing Cloudinary packages
93+
- ✅ **Install the latest**: When adding Cloudinary packages, use `npm install <package>` **with no version** so npm installs the latest compatible version (e.g. `npm install cloudinary-video-player`). In package.json use a **caret range** (e.g. `"cloudinary-video-player": "^1.0.0"`) so future installs get the latest compatible. Do not pin to an exact version unless you have verified it exists on npm.
94+
- ✅ **Package names only**: Use **only** these names: `@cloudinary/react`, `@cloudinary/url-gen`, `cloudinary-video-player` (standalone player), `cloudinary` (Node server-side only). Do not invent names (e.g. no `@cloudinary/video-player`).
95+
- ❌ **WRONG**: `npm install cloudinary-video-player@1.2.3` or `"cloudinary-video-player": "1.2.3"` (exact pin) — versions may not exist and break installs.
96+
- ✅ **Correct**: `npm install cloudinary-video-player` (no version) or in package.json: `"cloudinary-video-player": "^1.0.0"` (caret = latest compatible).
97+
9298
## Import Patterns
9399
- ✅ Import Cloudinary instance: `import { cld } from './cloudinary/config'`
94100
- ✅ Import components: `import { AdvancedImage, AdvancedVideo } from '@cloudinary/react'`
@@ -369,9 +375,8 @@ res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CL
369375
- Use when: user wants to show/display a video. Works with `cld.video()` like images with `cld.image()`
370376

371377
**2. Cloudinary Video Player** (`cloudinary-video-player`) — The **player**
372-
- Full-featured video player (styled UI, controls, playlists, recommendations, ads, chapters)
373-
- Use when: user asks for a "video player" or needs player features (playlists, ads, etc.)
374-
- Separate package; requires CSS and useEffect with `player.dispose()` cleanup
378+
- Full-featured video player (styled UI, controls, playlists). Use when the user asks for a "video player."
379+
- **Use imperative video element only** (create with document.createElement, append to container ref); do not pass a React-managed `<video ref>`. See "Cloudinary Video Player (The Player)" below.
375380

376381
### AdvancedVideo (React SDK - For Displaying a Video)
377382
- ✅ **Purpose**: Display a video with Cloudinary transformations (resize, effects, etc.). It is **not** a full player — it is for showing a video. For a player, use Cloudinary Video Player.
@@ -398,44 +403,41 @@ res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CL
398403
- ✅ **Documentation**: https://cloudinary.com/documentation/react_video_transformations
399404

400405
### Cloudinary Video Player (The Player)
401-
- ✅ **Purpose**: The actual video player — full-featured UI, controls, playlists, recommendations, ads, chapters. Use when the user asks for a "video player"; use AdvancedVideo when they just need to display a video.
402-
- ✅ **Package**: `cloudinary-video-player` (separate package)
403-
- ✅ **Import** (named, not default): `import { videoPlayer } from 'cloudinary-video-player'` and `import 'cloudinary-video-player/cld-video-player.min.css'`. **Do not use `dist/` in the CSS path** — use `cloudinary-video-player/cld-video-player.min.css` only.
404-
- ❌ **WRONG**: `import cloudinary from 'cloudinary-video-player'` then `cloudinary.videoPlayer(...)` — use named `videoPlayer` instead.
405-
- ✅ **player.source()** takes an **object**, not a string: `player.source({ publicId: 'samples/elephants' })`. ❌ WRONG: `player.source('samples/elephants')`.
406-
- ✅ **Use refs**, not IDs: `const videoRef = useRef<HTMLVideoElement>(null)`; pass `videoRef.current` to `videoPlayer()`. Avoid `document.getElementById` with React (can cause DOM conflicts).
407-
- ✅ **Store player in a ref**, not state: `const playerRef = useRef<ReturnType<typeof videoPlayer> | null>(null)`.
408-
- ✅ **Race condition (ref/DOM)**: useEffect runs right after render; at that moment `videoRef.current` may still be **null** (ref not attached yet) or the element may not be in the DOM. **Wait until** `videoRef.current` is set and ready before calling `videoPlayer(videoRef.current, ...)`. Options: (1) small delay (e.g. `setTimeout(..., 0)` or 50ms) then check `videoRef.current`; (2) `useLayoutEffect` so ref is committed before running; (3) poll until `videoRef.current?.isConnected`. Otherwise: "Invalid target for null#on; must be a DOM node or evented object".
409-
- ✅ **Initialize in useEffect** (or useLayoutEffect) with cleanup; **always dispose** in cleanup (wrap in try-catch so disposal errors don't throw).
410-
- ✅ **Validate env** before init: if `!import.meta.env.VITE_CLOUDINARY_CLOUD_NAME`, set error state and return early.
411-
- ✅ **Config**: `videoPlayer(element, { cloudName, secure: true, controls: true, fluid: true })`.
412-
- ✅ **Example** (refs, source object, wait for ref/DOM, cleanup with try-catch):
413-
```tsx
414-
const videoRef = useRef<HTMLVideoElement>(null);
415-
const playerRef = useRef<ReturnType<typeof videoPlayer> | null>(null);
416-
useEffect(() => {
417-
if (!cloudName) return;
418-
// Wait for ref to be attached and element in DOM (avoids "Invalid target for null#on")
419-
const timeoutId = setTimeout(() => {
420-
if (!videoRef.current) return;
421-
try {
422-
const player = videoPlayer(videoRef.current, { cloudName, secure: true, controls: true, fluid: true });
423-
player.source({ publicId: 'samples/elephants' }); // object, not string
424-
playerRef.current = player;
425-
} catch (err) { console.error(err); }
426-
}, 0);
427-
return () => {
428-
clearTimeout(timeoutId);
429-
if (playerRef.current) {
430-
try { playerRef.current.dispose(); } catch (e) { console.warn(e); }
431-
playerRef.current = null;
432-
}
433-
};
434-
}, [cloudName]);
435-
return <video ref={videoRef} className="cld-video-player cld-fluid" />;
436-
```
437-
- ✅ **Documentation**: https://cloudinary.com/documentation/cloudinary_video_player
438-
- ✅ **React Tutorial**: https://cloudinary.com/documentation/video_player_react_tutorial#banner
406+
Use when the user asks for a **video player** (styled UI, controls, playlists). For just **displaying** a video, use AdvancedVideo instead.
407+
408+
**Rule: imperative element only.** Do **not** pass a React-managed `<video ref={...} />` to the player — the library mutates the DOM and React will throw removeChild errors. Create the video element with `document.createElement('video')`, append it to a container ref, and pass that element to `videoPlayer(el, ...)`.
409+
410+
- **Package**: `cloudinary-video-player`. Install with `npm install cloudinary-video-player` (no version).
411+
- **Import**: `import { videoPlayer } from 'cloudinary-video-player'` (named) and `import 'cloudinary-video-player/cld-video-player.min.css'` (no `dist/` in path).
412+
- **player.source()** takes an **object**: `player.source({ publicId: 'samples/elephants' })`. Not a string.
413+
- **Cleanup**: Call `player.dispose()`, then **only if** `el.parentNode` exists call `el.parentNode.removeChild(el)` (avoids NotFoundError).
414+
- **If init fails** (CSP, extensions, timing): render **AdvancedVideo** with the same publicId. Do not relax CSP in index.html or ask the user to disable extensions.
415+
416+
**Example (copy this pattern):**
417+
```tsx
418+
const containerRef = useRef<HTMLDivElement>(null);
419+
const playerRef = useRef<ReturnType<typeof videoPlayer> | null>(null);
420+
useLayoutEffect(() => {
421+
if (!cloudName || !containerRef.current?.isConnected) return;
422+
const el = document.createElement('video');
423+
el.className = 'cld-video-player cld-fluid';
424+
containerRef.current.appendChild(el);
425+
try {
426+
const player = videoPlayer(el, { cloudName, secure: true, controls: true, fluid: true });
427+
player.source({ publicId: 'samples/elephants' });
428+
playerRef.current = player;
429+
} catch (err) { console.error(err); }
430+
return () => {
431+
if (playerRef.current) {
432+
try { playerRef.current.dispose(); } catch (e) { console.warn(e); }
433+
playerRef.current = null;
434+
}
435+
if (el.parentNode) el.parentNode.removeChild(el);
436+
};
437+
}, [cloudName]);
438+
return <div ref={containerRef} />;
439+
```
440+
Docs: https://cloudinary.com/documentation/cloudinary_video_player
439441

440442
### When to Use Which?
441443
- ✅ **Use AdvancedVideo** when: User wants to **display** or **show** a video (no full player). It just displays a video with transformations.
@@ -551,7 +553,7 @@ res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CL
551553
- ✅ Use `placeholder()` and `lazyload()` plugins together
552554
- ✅ Always add `width` and `height` attributes to `AdvancedImage`
553555
- ✅ Store `public_id` from upload success, not full URL
554-
- ✅ Always dispose video player in useEffect cleanup
556+
- ✅ Video player: use imperative element only; dispose in useLayoutEffect cleanup and remove element with `if (el.parentNode) el.parentNode.removeChild(el)`
555557
- ✅ Use TypeScript for better autocomplete and error catching
556558
- ✅ Prefer `unknown` over `any` when types aren't available
557559
- ✅ Use type guards for runtime type checking
@@ -665,10 +667,15 @@ res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CL
665667
- ✅ **Wait for script**: Before calling `window.cloudinary.createUploadWidget(...)`, ensure `typeof window.cloudinary?.createUploadWidget === 'function'`. If not ready, poll (e.g. setInterval until it exists) or inject the script in code and call createUploadWidget in the script's `onload`. Don't assume `window.cloudinary` means the API is ready.
666668
- ✅ See PATTERNS → Upload Widget Pattern ("Race condition") and Project setup → Upload Widget ("Wait for script").
667669

668-
### Video player: "Invalid target for null#on; must be a DOM node or evented object"
669-
- ❌ Problem: **Race condition** — useEffect runs right after render; at that moment `videoRef.current` may still be **null** (ref not attached) or the element may not be in the DOM. The player library requires a real DOM node.
670-
- ✅ **Wait for ref/DOM**: Before calling `videoPlayer(videoRef.current, ...)`, ensure `videoRef.current` is set. Use a short delay (e.g. `setTimeout(..., 0)` or 50ms) then check `videoRef.current`, or use `useLayoutEffect`, or poll until `videoRef.current?.isConnected`. Clean up the timeout in useEffect cleanup.
671-
- ✅ See PATTERNS → Cloudinary Video Player ("Race condition (ref/DOM)") and the example with `setTimeout(..., 0)`.
670+
### Video player: "Invalid target for null#on" or React removeChild or NotFoundError
671+
- ❌ Problem: Passing a React-managed `<video ref={...} />` to the player causes removeChild errors (the player mutates the DOM). Or container/ref not in DOM yet when init runs.
672+
- ✅ **Use imperative video element only**: Create the video with `document.createElement('video')`, append to a container ref, pass that element to `videoPlayer(el, ...)`. Check `containerRef.current?.isConnected` before init. In cleanup: dispose, then `if (el.parentNode) el.parentNode.removeChild(el)`. See PATTERNS → Cloudinary Video Player (The Player).
673+
674+
### Video player: failed HEAD or CORS-like console noise
675+
- Failed HEAD/analytics from the player does **not** necessarily mean playback fails. Do not add a preflight GET. If video doesn't play, use the imperative pattern and fall back to AdvancedVideo when init fails.
676+
677+
### Video player blocked by CSP or extensions
678+
- **Do not** relax CSP in index.html or ask the user to disable extensions. **Fall back to AdvancedVideo** with the same publicId when the player fails to initialize.
672679

673680
### User needs secure/signed uploads
674681
- ❌ Problem: User asks for secure uploads; unsigned preset or client-side secret is not acceptable.
@@ -707,14 +714,11 @@ res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CL
707714
4. If you need styled video player, use `cloudinary-video-player` instead
708715

709716
### "Video player not working" or "Player not initializing"
710-
- ❌ Problem: Configuration or initialization issue with standalone player
711-
- ✅ Solution:
712-
1. Check `cloud_name` is provided in player config
713-
2. Ensure CSS file is imported: `import 'cloudinary-video-player/cld-video-player.min.css'` (no `dist/` in path)
714-
3. Verify player is initialized in `useEffect` with proper cleanup
715-
4. Check video element has required classes: `cld-video-player cld-fluid`
716-
5. Always call `player.dispose()` in useEffect cleanup function
717-
6. For advanced features, ensure required modules are imported
717+
- ✅ **Use imperative video element only** (see PATTERNS → Cloudinary Video Player): createElement, append to container ref, pass to videoPlayer; cleanup: dispose then `if (el.parentNode) el.parentNode.removeChild(el)`. If init still fails (CSP, extensions), **fall back to AdvancedVideo** with the same publicId. Do not relax CSP or ask the user to disable extensions.
718+
719+
### Cloudinary package install fails or "version doesn't exist"
720+
- ❌ Problem: Agent pinned a Cloudinary package to a specific version (e.g. `cloudinary-video-player@1.2.3`) that doesn't exist on npm, or used a wrong package name.
721+
- ✅ **Install latest**: Use `npm install <package>` with **no version** so npm gets the latest compatible. In package.json use a **caret** (e.g. `"cloudinary-video-player": "^1.0.0"`). Use only correct package names: `@cloudinary/react`, `@cloudinary/url-gen`, `cloudinary-video-player`, `cloudinary`. See PATTERNS → "Installing Cloudinary packages".
718722

719723
### Confusion between AdvancedVideo and Video Player
720724
- **AdvancedVideo** = for **displaying** a video (not a full player). **Cloudinary Video Player** = the **player** (styled UI, controls, playlists, etc.).
@@ -736,8 +740,7 @@ res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CL
736740
```
737741

738742
### Video player: "source is not a function" or video not playing
739-
- ❌ Problem: Using `player.source(publicId)` with a string.
740-
- ✅ **player.source()** takes an **object**: `player.source({ publicId: 'samples/elephants' })`. Use named import: `import { videoPlayer } from 'cloudinary-video-player'` (not default `import cloudinary`). Use refs for the DOM element, not `document.getElementById`. See PATTERNS → Cloudinary Video Player (The Player).
743+
- **player.source()** takes an **object**: `player.source({ publicId: 'samples/elephants' })`, not a string. Use named import: `import { videoPlayer } from 'cloudinary-video-player'`. See PATTERNS → Cloudinary Video Player (The Player).
741744

742745
### Overlay: "Cannot read properties of undefined" or overlay not showing
743746
- ❌ Problem: Wrong overlay API usage (Overlay.source, compass constants, .transformation().resize, fontWeight on wrong object).
@@ -813,16 +816,14 @@ When something isn't working, check:
813816
- [ ] Plugins are in array format
814817
- [ ] Upload widget script is loaded in `index.html`
815818
- [ ] **"createUploadWidget is not a function"?** → Wait until `typeof window.cloudinary?.createUploadWidget === 'function'` before calling it (script loads async; poll or use script onload)
816-
- [ ] **Video player "Invalid target for null#on"?** → Wait for ref/DOM before calling videoPlayer (e.g. setTimeout 0 or 50ms, or useLayoutEffect); clean up timeout in useEffect cleanup
819+
- [ ] **Video player?** → **Imperative element only**: createElement('video'), append to container ref, pass to videoPlayer(el, ...); player.source({ publicId }); cleanup: dispose then if (el.parentNode) el.parentNode.removeChild(el). CSS: cloudinary-video-player/cld-video-player.min.css. If init fails, fall back to AdvancedVideo (do not relax CSP).
817820
- [ ] **Upload fails (unsigned)?** → Is `VITE_CLOUDINARY_UPLOAD_PRESET` set? Preset exists and is Unsigned in dashboard?
818821
- [ ] **Secure uploads?** → Use `uploadSignature` as function (not `signatureEndpoint`); fetch `api_key` from server first; include `uploadPreset` in widget config; server includes `upload_preset` in signed params; use Cloudinary Node SDK v2 on server; never expose or commit API secret
819822
- [ ] **Where do API key/secret go?** → **Do not** put in root `.env`. Use **`server/.env`**; add to `.gitignore`; load only in server. **Never commit** API key or secret
820823
- [ ] Upload preset is unsigned (for simple client uploads)
821-
- [ ] **Video player?** → Use named import `videoPlayer` from `cloudinary-video-player`; `player.source({ publicId })` (object, not string); use refs; dispose in cleanup with try-catch; CSS: `cloudinary-video-player/cld-video-player.min.css`
824+
- [ ] **Installing Cloudinary packages?** → Install latest: use `npm install <package>` with no version; in package.json use caret (^) so npm gets latest compatible; do not pin to exact versions
822825
- [ ] **Image overlays?** → Import `source` (not Overlay.source); `compass('south_east')` (strings with underscores); `new Transformation()` inside `.transformation()`; fontWeight on TextStyle, textColor on text source
823826
- [ ] **Image gallery?** → Use responsive/lazyload/placeholder plugins; use sample list (samples/cloudinary-icon, samples/bike, samples/landscapes/beach-boat, samples/food/spices, etc.); assume samples might not exist; use onError; prefer uploaded assets
824-
- [ ] Video player is disposed in cleanup (with try-catch)
825-
- [ ] CSS files are imported for video player
826827
- [ ] TypeScript types are properly imported
827828
- [ ] Upload result types are defined (not using `any`)
828829
- [ ] Environment variables are typed in `vite-env.d.ts`

0 commit comments

Comments
 (0)