You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: improve upload widget reliability and add video player poster options
- Add upload widget script to index.html (required, not just dynamic injection)
- Simplify UploadWidget to use React onClick instead of manual event listeners
- Add isReady state and loading indicator for better UX
- Fix type import for CloudinaryUploadResult (use import type)
- Add posterOptions to video player pattern (startOffset: '0', posterColor)
- Tighten upload widget rules: always poll, add timeout, no single onload check
- Fix plugin order inconsistency in rules
- Fix corrupted text in rules file
These changes address user feedback where upload widget would stop working
after adding features like video player due to script load timing races.
- Use **this** pattern for the reusable instance. Everywhere else: `import { cld } from './cloudinary/config'` (or the path the user chose) and call `cld.image(publicId)` / `cld.video(publicId)`.
49
49
50
50
**3. Upload Widget (unsigned, from scratch)**
51
-
- **Script**: Add to `index.html`: `<script src="https://upload-widget.cloudinary.com/global/all.js" async></script>`. Because the script loads **async**, React's useEffect can run before it's ready — **do not** call `createUploadWidget` until the script is loaded.
52
-
- **Wait for script**: Before calling `window.cloudinary.createUploadWidget(...)`, ensure `typeof window.cloudinary?.createUploadWidget === 'function'`. If not ready, poll (e.g. setInterval) or wait for the script's `onload` (if you inject the script in code). Otherwise you get "createUploadWidget is not a function".
53
-
- **Create widget in useEffect**, not in render. Store the widget in a **ref**. Pass options: `{ cloudName: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME, uploadPreset: uploadPreset || undefined, sources: ['local', 'camera', 'url'], multiple: false }`. Use `uploadPreset` from config or env.
54
-
- **Open on click**: Attach a click listener to a button that calls `widgetRef.current?.open()`. Remove the listener in useEffect cleanup. Handle script load failures (e.g. show error state if script never loads).
51
+
52
+
**Strict pattern (always follow this exactly):**
53
+
1. **Script in `index.html`** (required): Add `<script src="https://upload-widget.cloudinary.com/global/all.js" async></script>` to `index.html`. Do **not** rely only on dynamic script injection from React — it's fragile.
54
+
2. **Poll in useEffect** (required): In `useEffect`, poll with `setInterval` (e.g. every 100ms) until `typeof window.cloudinary?.createUploadWidget === 'function'`. Only then create the widget. A single check (even in `onload`) is **not** reliable because `window.cloudinary` can exist before `createUploadWidget` is attached.
55
+
3. **Add a timeout**: Set a timeout (e.g. 10 seconds) to stop polling and show an error if the script never loads. Clear both interval and timeout in cleanup.
56
+
4. **Create widget once**: When `createUploadWidget` is available, create the widget and store it in a **ref**. Clear the interval and timeout. Pass options: `{ cloudName, uploadPreset, sources: ['local', 'camera', 'url'], multiple: false }`.
57
+
5. **Open on click**: Attach a click listener to a button that calls `widgetRef.current?.open()`. Remove the listener in useEffect cleanup.
58
+
59
+
❌ **Do NOT**: Check only `window.cloudinary` (not enough); do a single check in `onload` (unreliable); skip the script in `index.html`; poll forever without a timeout.
55
60
- **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).
- **Env**: Use your bundler's client env prefix and access (Vite: `VITE_` + `import.meta.env.VITE_*`; see "Other bundlers" if not Vite).
62
67
- **Reusable instance**: One config file that creates and exports `cld` (and optionally `uploadPreset`) from `@cloudinary/url-gen`; use it everywhere.
63
-
- **Upload widget**: Script in index.html (or equivalent); create widget once in useEffect with ref; unsigned = cloudName + uploadPreset; signed = use uploadSignature function and backend.
68
+
- **Upload widget**: Script in index.html (required); in useEffect, **poll** until `createUploadWidget` is a function, then create widget once and store in ref; unsigned = cloudName + uploadPreset; signed = use uploadSignature function and backend.
64
69
- **Video player**: Imperative video element (createElement, append to container ref, pass to videoPlayer); dispose + removeChild in cleanup; fall back to AdvancedVideo if init fails.
65
70
66
71
**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.
@@ -201,16 +206,16 @@ cld.image('id').overlay(
201
206
- ✅ Import plugins from `@cloudinary/react`
202
207
- ✅ Pass plugins as array: `plugins={[responsive(), lazyload(), placeholder()]}`
203
208
- ✅ Recommended plugin order:
204
-
1. `responsive()` - First
205
-
2. `lazyload()` - Second
206
-
3. `accessibility()` - Third
207
-
4. `placeholder()` - Last
209
+
1. `responsive()` - First (handles breakpoints)
210
+
2. `placeholder()` - Second (shows placeholder while loading)
211
+
3. `lazyload()` - Third (delays loading until in viewport)
212
+
4. `accessibility()` - Last (if needed)
208
213
- ✅ Always add `width` and `height` attributes to prevent layout shift
- ✅ **Race condition**: The script loads **async**, so React's useEffect may run before `createUploadWidget` exists. **Wait until** `typeof window.cloudinary?.createUploadWidget === 'function'` before calling it (e.g. poll with setInterval or wait for script onload). Checking only `window.cloudinary` is not enough — `createUploadWidget` might not be attached yet. Otherwise: "createUploadWidget is not a function".
264
+
2. ✅ **Poll in useEffect until `createUploadWidget` is available** (required): Use `setInterval` (e.g. every 100ms) to check `typeof window.cloudinary?.createUploadWidget === 'function'`. Only create the widget when this returns `true`. Clear the interval once ready.
265
+
3. ✅ **Add a timeout** (e.g. 10 seconds) to stop polling and show an error state if the script never loads. Clear both interval and timeout in cleanup and when ready.
266
+
4. ✅ **Create widget once**, store in a ref. Cleanup: clear interval, clear timeout, remove click listener.
267
+
268
+
❌ **Do NOT**: Check only `window.cloudinary` (the function may not be attached yet); do a single check in `onload` (unreliable timing); skip `index.html` and rely only on dynamic injection; poll forever without a timeout.
269
+
258
270
- ✅ Create unsigned upload preset in dashboard at `settings/upload/presets`
259
271
- ✅ Add to `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your_preset_name`
260
272
- ✅ Handle callbacks:
@@ -413,6 +425,11 @@ Use when the user asks for a **video player** (styled UI, controls, playlists).
- **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
427
428
+
**Poster options**: Always include `posterOptions` for a predictable poster image with a fallback color:
429
+
- `transformation: { startOffset: '0' }` — use the first frame of the video as the poster (consistent and loads reliably)
430
+
- `posterColor: '#0f0f0f'` — if the poster image fails to load, shows a dark background instead of blank/broken
431
+
- These can be overridden via props (e.g. `posterOptions={{ transformation: { startOffset: '5' } }}` for a different frame)
- ✅ Use `placeholder()` and `lazyload()` plugins together
554
590
- ✅ Always add `width` and `height` attributes to `AdvancedImage`
555
591
- ✅ Store `public_id` from upload success, not full URL
556
-
- ✅ Video player: use imperative element only; dispose in useLayoutEffect cleanup and remove element with `if (el.parentNode) el.parentNode.removeChild(el)`
592
+
- ✅ Video player: use imperative element only; dispose in useLayoutEffect cleanup and remove element with `if (el.parentNode) el.parentNode.removeChild(el)`; always include `posterOptions` with `transformation: { startOffset: '0' }` and `posterColor: '#0f0f0f'` for reliable poster display
557
593
- ✅ Use TypeScript for better autocomplete and error catching
558
594
- ✅ Prefer `unknown` over `any` when types aren't available
4. Consider using server-side upload for very large files
657
693
658
694
### Widget not opening
659
-
- ❌ Problem: Script not loaded or initialization issue
695
+
- ❌ Problem: Script not loaded, or widget created before `createUploadWidget` was available
660
696
- ✅ Solution:
661
697
1. Ensure script is in `index.html`: `<script src="https://upload-widget.cloudinary.com/global/all.js" async></script>`
662
-
2. Check widget initializes in `useEffect` after `window.cloudinary` is available
698
+
2. In `useEffect`, **poll** with `setInterval` until `typeof window.cloudinary?.createUploadWidget === 'function'` — only then create the widget. Do **not** check only `window.cloudinary`.
663
699
3. Verify upload preset is set correctly
664
700
665
701
### "createUploadWidget is not a function"
666
-
- ❌ Problem: **Race condition** — the script in index.html loads **async**, so React's useEffect can run before the script has finished loading. `window.cloudinary` might exist but `createUploadWidget` isn't attached yet.
667
-
- ✅ **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.
668
-
- ✅ See PATTERNS → Upload Widget Pattern ("Race condition") and Project setup → Upload Widget ("Wait for script").
702
+
- ❌ Problem: **Race condition** — the script loads **async**, so `window.cloudinary` can exist before `createUploadWidget` is attached. A single check (even in `onload`) is **not** reliable.
703
+
- ✅ **Always poll**: In `useEffect`, use `setInterval` to check `typeof window.cloudinary?.createUploadWidget === 'function'`. Only create the widget when this returns `true`. Clear the interval once ready.
704
+
- ❌ **Do NOT**: Check only `window.cloudinary`; do a single check in `onload`; skip the script in `index.html`.
705
+
- ✅ See PATTERNS → Upload Widget Pattern and Project setup → Upload Widget for the strict pattern.
669
706
670
707
### Video player: "Invalid target for null#on" or React removeChild or NotFoundError
671
708
- ❌ 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.
### Cloudinary package install fails or "version doesn't exist"
720
757
- ❌ 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/tps://console.cloudinary.com/app/settings/upload/presets\n\n'-gen`, `cloudinary-video-player`, `cloudinary`. See PATTERNS → "Installing Cloudinary packages".
758
+
- ✅ **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".
722
759
723
760
### Confusion between AdvancedVideo and Video Player
724
761
- **AdvancedVideo** = for **displaying** a video (not a full player). **Cloudinary Video Player** = the **player** (styled UI, controls, playlists, etc.).
### Video player: "source is not a function" or video not playing
743
780
- **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).
744
781
782
+
### Video player: poster image missing, wrong frame, or broken
783
+
- ❌ Problem: Video player shows no poster, wrong poster frame, or blank area before video loads.
784
+
- ✅ **Always include `posterOptions`** in the player config: `posterOptions: { transformation: { startOffset: '0' }, posterColor: '#0f0f0f' }`. This uses the first frame as the poster (reliable) and provides a dark fallback color if the poster fails to load.
785
+
- ✅ **Override if needed**: Pass different values via props, e.g. `startOffset: '5'` for a frame 5 seconds in, or a different `posterColor` for your design.
786
+
745
787
### Overlay: "Cannot read properties of undefined" or overlay not showing
746
788
- ❌ Problem: Wrong overlay API usage (Overlay.source, compass constants, .transformation().resize, fontWeight on wrong object).
747
789
- ✅ Import `source` directly from `@cloudinary/url-gen/actions/overlay` (not `Overlay.source`). Use **string** values for compass: `compass('south_east')` (underscores, not camelCase). Use `new Transformation()` inside `.transformation()`. Put `fontWeight` on **TextStyle**; put `textColor` on the **text source**. See PATTERNS → Image Overlays (text or logos).
@@ -815,8 +857,8 @@ When something isn't working, check:
815
857
- [ ] Format/quality use separate `.delivery()` calls
816
858
- [ ] Plugins are in array format
817
859
- [ ] Upload widget script is loaded in `index.html`
818
-
- [ ] **"createUploadWidget is not a function"?** → Wait until `typeof window.cloudinary?.createUploadWidget === 'function'` before calling it (script loads async; poll or use script onload)
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).
860
+
- [ ] **"createUploadWidget is not a function"?** → In useEffect, **poll** with setInterval until `typeof window.cloudinary?.createUploadWidget === 'function'`. Do NOT check only `window.cloudinary`; do NOT rely on a single onload check
861
+
- [ ] **Video player?** → **Imperative element only**: createElement('video'), append to container ref, pass to videoPlayer(el, ...); include `posterOptions: { transformation: { startOffset: '0' }, posterColor: '#0f0f0f' }` for reliable poster; 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).
820
862
- [ ] **Upload fails (unsigned)?** → Is `VITE_CLOUDINARY_UPLOAD_PRESET` set? Preset exists and is Unsigned in dashboard?
821
863
- [ ] **Upload default?** → Default to **unsigned** uploads (cloudName + uploadPreset); use signed only when the user explicitly asks for secure/signed uploads (signed requires a running backend)
822
864
- [ ] **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
0 commit comments