create-cloudinary-react 1.0.0-beta.7 → 1.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -171,8 +171,8 @@ jobs:
171
171
  echo "Publishing to npm..."
172
172
 
173
173
  # Publish using npm publish which supports OIDC/trusted publishing
174
- # --tag beta required for prerelease versions (1.0.0-beta.x)
175
- npm publish --provenance --access public --tag beta
174
+ # --tag latest so installers get the most recent version (npm i create-cloudinary-react / npx create-cloudinary-react)
175
+ npm publish --provenance --access public --tag latest
176
176
  echo "✓ Published $VERSION_AFTER to npm"
177
177
  else
178
178
  echo "No version change detected (version: $VERSION_AFTER)"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # [1.0.0-beta.8](https://github.com/cloudinary-devs/create-cloudinary-react/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2026-01-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * publish to latest tag so installers get most recent version ([d907c93](https://github.com/cloudinary-devs/create-cloudinary-react/commit/d907c933a1d0cd7ba83512725f05c453cce77d9f))
7
+ * url-gen imports, overlay rules, and upload widget race condition ([08f06af](https://github.com/cloudinary-devs/create-cloudinary-react/commit/08f06af90ade2f957e3a1997afe352c145c65b82))
8
+
1
9
  # [1.0.0-beta.7](https://github.com/cloudinary-devs/create-cloudinary-react/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2026-01-27)
2
10
 
3
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cloudinary-react",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.8",
4
4
  "description": "Scaffold a Cloudinary React + Vite + TypeScript project with interactive setup",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,7 @@
1
1
  # Cloudinary React SDK Patterns & Common Errors
2
2
 
3
+ **Scope**: These rules apply to **React (web)** with the browser Upload Widget. The **default** is **Vite** (create-cloudinary-react uses Vite). They also work with **other bundlers** (Create React App, Next.js, Parcel, etc.): only **how you read env vars** changes; see **"Other bundlers (non-Vite)"** below. Rules-only users: see **"Project setup (rules-only / without CLI)"** for the reusable Cloudinary instance, env, Upload Widget (unsigned/signed), and video player. For **React Native** uploads (including signed upload), see: https://cloudinary.com/documentation/react_native_image_and_video_upload#signed_upload — same “never expose secret, generate signature on backend” principle, but React Native uses the `upload()` method and backend SDKs differently.
4
+
3
5
  ## Official Documentation
4
6
  - **Transformation Rules**: https://cloudinary.com/documentation/cloudinary_transformation_rules.md
5
7
  - **Transformation Reference**: https://cloudinary.com/documentation/transformation_reference
@@ -7,46 +9,156 @@
7
9
  - **React Video Transformations**: https://cloudinary.com/documentation/react_video_transformations
8
10
  - **Cloudinary Video Player** (standalone player): https://cloudinary.com/documentation/cloudinary_video_player
9
11
  - **Video Player React Tutorial**: https://cloudinary.com/documentation/video_player_react_tutorial#banner
12
+ - **Upload Widget (signed uploads)**: https://cloudinary.com/documentation/upload_widget#signed_uploads
13
+ - **Upload assets in Next.js (backend signature)**: https://cloudinary.com/documentation/upload_assets_in_nextjs_tutorial
14
+ - **Cloudinary Node.js SDK (server-side signing)** — use **v2**: `import { v2 as cloudinary } from 'cloudinary'`; do not use v1 (e.g. 1.47.0). https://cloudinary.com/documentation/node_integration
15
+ - **React Native image and video upload (signed)**: https://cloudinary.com/documentation/react_native_image_and_video_upload#signed_upload
10
16
  - Always consult the official transformation rules when creating transformations
11
17
  - Use only officially supported parameters from the transformation reference
12
18
 
19
+ **Golden rule for @cloudinary/url-gen:** When generating overlay or transformation code, use **only** the "Import reference" table and "Canonical overlay block" in these rules. Copy those import paths and usage patterns exactly; do not guess or derive paths (e.g. `text` and `image` are from `qualifiers/source`, not `actions/overlay`; `Position` is from `qualifiers/position`, not `positioning/Position`).
20
+
13
21
  ---
14
22
 
15
23
  # 📋 PATTERNS (How to Do It Right)
16
24
 
25
+ ## Project setup (rules-only / without CLI)
26
+
27
+ If the user is **not** using the create-cloudinary-react CLI and only has these rules, generate the following so they get correct config, env, and widget setup.
28
+
29
+ **1. Environment (.env)**
30
+ Create a `.env` file in the project root with **Vite prefix** (required for client access):
31
+ - `VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name` (required)
32
+ - `VITE_CLOUDINARY_UPLOAD_PRESET=your_unsigned_preset_name` (optional; required for unsigned upload widget)
33
+ - Restart the dev server after adding or changing `.env`. Use `import.meta.env.VITE_*` in code, not `process.env`.
34
+
35
+ **2. Reusable Cloudinary instance (config)**
36
+ Create a config file (e.g. `src/cloudinary/config.ts`) so the rest of the app can use a single `cld` instance:
37
+ ```ts
38
+ import { Cloudinary } from '@cloudinary/url-gen';
39
+
40
+ const cloudName = import.meta.env.VITE_CLOUDINARY_CLOUD_NAME;
41
+ if (!cloudName) {
42
+ throw new Error('VITE_CLOUDINARY_CLOUD_NAME is not set. Add it to .env with the VITE_ prefix.');
43
+ }
44
+
45
+ export const cld = new Cloudinary({ cloud: { cloudName } });
46
+ export const uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || '';
47
+ ```
48
+ - 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
+
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).
55
+ - **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).
56
+
57
+ **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).
59
+
60
+ **5. Summary for rules-only users**
61
+ - **Env**: Use your bundler's client env prefix and access (Vite: `VITE_` + `import.meta.env.VITE_*`; see "Other bundlers" if not Vite).
62
+ - **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.
64
+ - **Video player**: Named import `videoPlayer`, source object, refs, dispose in cleanup.
65
+
66
+ **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.
67
+
17
68
  ## Environment Variables
18
- - **Vite requires VITE_ prefix** - Environment variables MUST start with `VITE_` to be exposed to the browser
19
- - ✅ CORRECT: `VITE_CLOUDINARY_CLOUD_NAME=mycloud` in `.env` file
20
- - ✅ CORRECT: `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` (not `process.env`)
21
- - Always restart dev server after adding/updating `.env` variables
69
+ - **Default: Vite** — Vite requires `VITE_` prefix; use `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` (not `process.env`). Restart dev server after changing `.env`.
70
+ - ✅ CORRECT (Vite): `VITE_CLOUDINARY_CLOUD_NAME=mycloud` in `.env`; `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME`
71
+
72
+ ## Other bundlers (non-Vite)
73
+ - **Only the env access changes.** All other patterns (reusable `cld`, Upload Widget, Video Player, overlays, signed uploads) are bundler-agnostic.
74
+ - **Create React App**: Prefix `REACT_APP_`; access `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`, `process.env.REACT_APP_CLOUDINARY_UPLOAD_PRESET`. Restart dev server after `.env` changes.
75
+ - **Next.js (client)**: Prefix `NEXT_PUBLIC_` for client; access `process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`, etc. Server-side can use `process.env.CLOUDINARY_*` without `NEXT_PUBLIC_`.
76
+ - **Parcel / other**: Check the bundler's docs for "exposing environment variables to the client" (often a prefix or allowlist). Use that prefix and the documented access (e.g. `process.env.*`).
77
+ - **Config file**: In `src/cloudinary/config.ts` (or equivalent), read cloud name and upload preset using the **user's bundler** env API (e.g. for CRA: `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`). Same `new Cloudinary({ cloud: { cloudName } })` and exports; only the env read line changes.
22
78
 
23
79
  ## Upload Presets
24
- - **Unsigned upload presets are required for client-side uploads** - Transformations work without them, but uploads need an unsigned preset
25
- - ✅ Create unsigned upload preset: https://console.cloudinary.com/app/settings/upload/presets
80
+ - **Unsigned** = client-only uploads (no backend). **Signed** = backend required, more secure. See **"Signed vs unsigned uploads"** below for when to use which.
81
+ - ✅ Create unsigned upload preset (for simple client uploads): https://console.cloudinary.com/app/settings/upload/presets
26
82
  - ✅ Set preset in `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name`
27
83
  - ✅ Use in code: `import { uploadPreset } from './cloudinary/config'`
28
84
  - ⚠️ If upload preset is missing, the Upload Widget will show an error message
29
85
  - ⚠️ Upload presets must be set to "Unsigned" mode for client-side usage (no API key/secret needed)
86
+ - **When unsigned upload fails**: First check that the user configured their upload preset:
87
+ 1. Is `VITE_CLOUDINARY_UPLOAD_PRESET` set in `.env`? (must match preset name exactly)
88
+ 2. Does the preset exist in the dashboard under Settings → Upload → Upload presets?
89
+ 3. Is the preset set to **Unsigned** (not Signed)?
90
+ 4. Was the dev server restarted after adding/updating `.env`?
30
91
 
31
92
  ## Import Patterns
32
93
  - ✅ Import Cloudinary instance: `import { cld } from './cloudinary/config'`
33
94
  - ✅ Import components: `import { AdvancedImage, AdvancedVideo } from '@cloudinary/react'`
34
- - ✅ Import transformations: `import { fill } from '@cloudinary/url-gen/actions/resize'`
35
- - ✅ Import effects: `import { blur } from '@cloudinary/url-gen/actions/effect'`
36
- - ✅ Import delivery: `import { format, quality } from '@cloudinary/url-gen/actions/delivery'`
37
- - ✅ Import qualifiers: `import { auto } from '@cloudinary/url-gen/qualifiers/format'`
38
- - ✅ Import qualifiers: `import { auto as autoQuality } from '@cloudinary/url-gen/qualifiers/quality'`
39
95
  - ✅ Import plugins: `import { responsive, lazyload, placeholder } from '@cloudinary/react'`
96
+ - ✅ **For transformations and overlays**, use **only** the exact paths in "Import reference: @cloudinary/url-gen" and the "Canonical overlay block" below. Do **not** guess subpaths (e.g. `text` and `image` are from `qualifiers/source`, not `actions/overlay`).
97
+
98
+ ## Import reference: @cloudinary/url-gen (use these exact paths only)
99
+
100
+ **Rule:** Do not invent or guess import paths for `@cloudinary/url-gen`. Use **only** the paths in the table and canonical block below. Copy the import statements exactly; do not derive paths (e.g. `@cloudinary/url-gen/overlay` exports only `source` — `text` and `image` are from **`qualifiers/source`**; `Position` is from **`qualifiers/position`**, not `positioning/Position`). Wrong paths cause "module not found" or "does not exist".
101
+
102
+ | Purpose | Exact import |
103
+ |--------|----------------|
104
+ | Cloudinary instance (config) | `import { Cloudinary } from '@cloudinary/url-gen';` |
105
+ | Resize (fill) | `import { fill } from '@cloudinary/url-gen/actions/resize';` |
106
+ | Resize (scale, for overlays) | `import { scale } from '@cloudinary/url-gen/actions/resize';` |
107
+ | Delivery format/quality | `import { format, quality } from '@cloudinary/url-gen/actions/delivery';` |
108
+ | Format qualifier (auto) | `import { auto } from '@cloudinary/url-gen/qualifiers/format';` |
109
+ | Quality qualifier (auto) | `import { auto as autoQuality } from '@cloudinary/url-gen/qualifiers/quality';` |
110
+ | Effects (e.g. blur) | `import { blur } from '@cloudinary/url-gen/actions/effect';` |
111
+ | Overlay source | `import { source } from '@cloudinary/url-gen/actions/overlay';` |
112
+ | Overlay text / image (source types) | `import { text, image } from '@cloudinary/url-gen/qualifiers/source';` |
113
+ | Overlay image transformation | `import { Transformation } from '@cloudinary/url-gen/transformation/Transformation';` |
114
+ | Position (overlay) | `import { Position } from '@cloudinary/url-gen/qualifiers/position';` |
115
+ | Gravity/compass | `import { compass } from '@cloudinary/url-gen/qualifiers/gravity';` |
116
+ | Text style (overlay) | `import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle';` |
117
+ | Types | `import type { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';` |
118
+
119
+ **Canonical overlay block (copy these imports and patterns exactly):**
120
+ ```ts
121
+ // Overlay imports — text/image from qualifiers/source, NOT actions/overlay
122
+ import { source } from '@cloudinary/url-gen/actions/overlay';
123
+ import { text, image } from '@cloudinary/url-gen/qualifiers/source';
124
+ import { Position } from '@cloudinary/url-gen/qualifiers/position';
125
+ import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle';
126
+ import { compass } from '@cloudinary/url-gen/qualifiers/gravity';
127
+ import { Transformation } from '@cloudinary/url-gen/transformation/Transformation';
128
+ import { scale } from '@cloudinary/url-gen/actions/resize';
129
+
130
+ // Text overlay (compass with underscores: 'south_east', 'center')
131
+ cld.image('id').overlay(
132
+ source(text('Hello', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white'))
133
+ .position(new Position().gravity(compass('center')))
134
+ );
135
+
136
+ // Image overlay (logo/image with resize)
137
+ cld.image('id').overlay(
138
+ source(image('logo').transformation(new Transformation().resize(scale().width(100).height(100))))
139
+ .position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20))
140
+ );
141
+ ```
142
+
143
+ - **Components** (AdvancedImage, AdvancedVideo, plugins) come from **`@cloudinary/react`**, not from `@cloudinary/url-gen`.
144
+ - **Transformation actions and qualifiers** (resize, delivery, effect, overlay, etc.) come from **`@cloudinary/url-gen/actions/*`** and **`@cloudinary/url-gen/qualifiers/*`** with the exact subpaths above.
145
+ - If an import fails, verify the package version (`@cloudinary/url-gen` in package.json) and the [Cloudinary URL-Gen SDK docs](https://cloudinary.com/documentation/sdks/js/url-gen/index.html) or [Transformation Builder reference](https://cloudinary.com/documentation/sdks/js/transformation_builder_reference).
40
146
 
41
147
  ## Creating Image & Video Instances
42
148
  - ✅ Create image instance: `const img = cld.image(publicId)`
43
149
  - ✅ Create video instance: `const video = cld.video(publicId)` (same pattern as images)
44
150
  - ✅ Public ID format: Use forward slashes for folders (e.g., `'folder/subfolder/image'`)
45
151
  - ✅ Public IDs are case-sensitive and should not include file extensions
152
+ - ✅ **Sample assets**: Cloudinary may provide sample assets under `samples/`. **Assume they might not exist** (users can delete them); always handle load errors and provide fallbacks (see Image gallery). When they exist, use them for examples and demos instead of requiring uploads first.
153
+ - ✅ **Sample public IDs that may be available** (use for galleries, demos; handle onError if missing):
154
+ - Images: `samples/cloudinary-icon`, `samples/two-ladies`, `samples/food/spices`, `samples/landscapes/beach-boat`, `samples/bike`, `samples/landscapes/girl-urban-view`, `samples/animals/reindeer`, `samples/food/pot-mussels`
155
+ - Video: `samples/elephants`
156
+ - ✅ **Default / most reliable**: Start with `samples/cloudinary-icon` for a single image; use the list above for galleries or variety. Prefer uploaded assets when the user has them.
46
157
  - ✅ Examples:
47
158
  ```tsx
48
159
  const displayImage = cld.image('samples/cloudinary-icon');
49
160
  const displayVideo = cld.video('samples/elephants');
161
+ // Gallery: e.g. ['samples/bike', 'samples/landscapes/beach-boat', 'samples/food/spices', ...]
50
162
  ```
51
163
 
52
164
  ## Transformation Patterns
@@ -79,6 +191,7 @@
79
191
  - ✅ Same transformation syntax works for both images and videos
80
192
 
81
193
  ## Plugin Patterns
194
+ - ✅ **When the user asks for lazy loading or responsive images**: Use the **Cloudinary plugins** from `@cloudinary/react` — `responsive()`, `lazyload()`, `placeholder()` — with `AdvancedImage`. Do not use only native `loading="lazy"` or CSS-only responsive; the Cloudinary plugins handle breakpoints, lazy loading, and placeholders for Cloudinary URLs.
82
195
  - ✅ Import plugins from `@cloudinary/react`
83
196
  - ✅ Pass plugins as array: `plugins={[responsive(), lazyload(), placeholder()]}`
84
197
  - ✅ Recommended plugin order:
@@ -98,6 +211,7 @@
98
211
  ```
99
212
 
100
213
  ## Responsive Images Pattern
214
+ - ✅ **Responsive images**: Use the Cloudinary `responsive()` plugin with `fill()` resize (not only CSS). **Lazy loading**: Use the Cloudinary `lazyload()` plugin with `AdvancedImage` (not only `loading="lazy"`).
101
215
  - ✅ Use `responsive()` plugin with `fill()` resize
102
216
  - ✅ Combine with `placeholder()` and `lazyload()` plugins
103
217
  - ✅ Example:
@@ -111,12 +225,30 @@
111
225
  />
112
226
  ```
113
227
 
228
+ ## Image gallery with lazy loading and responsive
229
+ - ✅ **When the user asks for an image gallery with lazy loading and responsive**: Use Cloudinary **plugins** with `AdvancedImage`: `responsive()`, `lazyload()`, `placeholder()` (see Plugin Patterns). Use `fill()` resize with the responsive plugin. Add `width` and `height` to prevent layout shift.
230
+ - ✅ **Sample assets in galleries**: Use the sample public IDs from "Creating Image & Video Instances" (e.g. `samples/bike`, `samples/landscapes/beach-boat`, `samples/food/spices`, `samples/two-ladies`, `samples/landscapes/girl-urban-view`, `samples/animals/reindeer`, `samples/food/pot-mussels`, `samples/cloudinary-icon`). **Assume any sample might not exist** — users can delete them. Start with one reliable sample (e.g. `samples/cloudinary-icon`) or a short list; add **onError** handling and remove/hide failed images. Prefer **uploaded** assets when available (e.g. from UploadWidget) over samples.
231
+ - ✅ **Handle load errors**: Use `onError` on `AdvancedImage` to hide or remove failed images (e.g. set state to filter out the publicId, or hide the parent). Provide user feedback (e.g. "Some images could not be loaded. Try uploading your own!") and upload functionality so users can add their own images.
232
+ - ✅ **Fallback**: Default gallery list can be a subset of the sample list (e.g. `['samples/cloudinary-icon', 'samples/bike', 'samples/landscapes/beach-boat']`); when user uploads, append `result.public_id`. If an image fails to load, remove it from the list or hide it so the UI doesn't show broken images.
233
+
234
+ ## Image Overlays (text or logos)
235
+ - ✅ **When the user asks for image overlays with text or logos**: Use `@cloudinary/url-gen` overlay APIs. Copy imports and patterns from the **"Import reference"** table and **"Canonical overlay block"** in these rules. Do not import `text` or `image` from `actions/overlay` — they are from **`qualifiers/source`**; only `source` is from `actions/overlay`.
236
+ - ✅ **Import** `source` from `actions/overlay`; **`text` and `image` from `qualifiers/source`**. Also: `Position` from `qualifiers/position`, `TextStyle` from `qualifiers/textStyle`, `compass` from `qualifiers/gravity`, `Transformation` from `transformation/Transformation`, `scale` from `actions/resize`.
237
+ - ✅ **compass()** takes **string** values, with **underscores**: `compass('center')`, `compass('south_east')`, `compass('north_west')`. ❌ WRONG: `compass(southEast)` or `'southEast'` (no camelCase).
238
+ - ✅ **Overlay image**: Use `new Transformation()` **inside** `.transformation()`: `image('logo').transformation(new Transformation().resize(scale().width(100).height(100)))`. ❌ WRONG: `image('logo').transformation().resize(...)` (`.transformation()` does not return a chainable object).
239
+ - ✅ **Text overlay**: `fontWeight` goes on **TextStyle**: `new TextStyle('Arial', 60).fontWeight('bold')`. `textColor` goes on the **text source** (chained after `text(...)`): `text('Hello', new TextStyle('Arial', 60)).textColor('white')`.
240
+ - ✅ **Position** is chained **after** `source(...)`, not inside: `source(image('logo').transformation(...)).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20))`.
241
+ - ✅ **Image overlay pattern**: `baseImage.overlay(source(image('id').transformation(new Transformation().resize(scale().width(100).height(100)))).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20)))`. (Import `scale` from `@cloudinary/url-gen/actions/resize` if needed.)
242
+ - ✅ **Text overlay pattern**: `baseImage.overlay(source(text('Your Text', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white')).position(new Position().gravity(compass('center'))))`.
243
+ - ✅ Docs: React Image Transformations and transformation reference for overlay syntax.
244
+
114
245
  ## Upload Widget Pattern
115
246
  - ✅ Use component: `import { UploadWidget } from './cloudinary/UploadWidget'`
116
247
  - ✅ Load script in `index.html`:
117
248
  ```html
118
249
  <script src="https://upload-widget.cloudinary.com/global/all.js" async></script>
119
250
  ```
251
+ - ✅ **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".
120
252
  - ✅ Create unsigned upload preset in dashboard at `settings/upload/presets`
121
253
  - ✅ Add to `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your_preset_name`
122
254
  - ✅ Handle callbacks:
@@ -132,24 +264,117 @@
132
264
  ```
133
265
  - ✅ Upload result contains: `public_id`, `secure_url`, `width`, `height`, etc.
134
266
 
267
+ ## Signed vs unsigned uploads (when to use which)
268
+
269
+ **Unsigned uploads** (simpler, no backend required):
270
+ - Use when: Quick prototypes, low-risk apps, or when anyone with the preset name may upload.
271
+ - Preset: Create an **Unsigned** upload preset in Cloudinary dashboard (Settings → Upload → Upload presets). Put preset name in `.env` as `VITE_CLOUDINARY_UPLOAD_PRESET`.
272
+ - Client: Widget needs only `cloudName` and `uploadPreset`. No API key or secret; no backend.
273
+ - Trade-off: Anyone who knows the preset name can upload. Use only when that is acceptable.
274
+
275
+ **Signed uploads** (more secure, backend required):
276
+ - Use when: Production apps, authenticated users, or when you need to control who can upload.
277
+ - Preset: Create a **Signed** upload preset in the dashboard. The backend generates a signature using your API secret; the client never sees the secret.
278
+ - Client: Widget gets `api_key` (from your backend), `uploadPreset`, and an `uploadSignature` **function** that calls your backend for each upload. API secret stays on server only.
279
+ - Trade-off: Requires a backend (Node/Express, Next.js API route, etc.) to sign requests. More secure; signature validates each upload.
280
+
281
+ **Rule of thumb**: If the user asks for "secure" or "signed" uploads, or needs to restrict uploads, use **signed** with a backend. For simple demos or when preset exposure is acceptable, **unsigned** is fine.
282
+
283
+ ## Secure (Signed) Uploads
284
+
285
+ **Golden rules**: (1) **Never expose or commit the API secret** — it must live only in server env and server code. (2) **Never commit the API key or secret** — use `server/.env` (or equivalent) and ensure it is in `.gitignore`. (3) The **api_key** is not secret and may be sent to the client (e.g. in the signature response); only **api_secret** must stay server-only.
286
+
287
+ **When the user asks for secure uploads**: Use a signed upload preset and generate the signature on the server. The client may receive `uploadSignature`, `uploadSignatureTimestamp`, `api_key`, and `cloudName` from your backend; it must **never** receive or contain the API secret.
288
+
289
+ ### Where to put API key and secret (server-only, never committed)
290
+
291
+ - **Do not put them in the root `.env`** used by Vite. Keep root `.env` for `VITE_CLOUDINARY_CLOUD_NAME` and `VITE_CLOUDINARY_UPLOAD_PRESET` only.
292
+ - **Create `server/.env`** (in a `server/` folder) and put there: `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET`. No `VITE_` prefix. Load this file only in the server process (e.g. `dotenv.config({ path: 'server/.env' })`).
293
+ - **Never commit API key or secret**: Add `server/.env` to `.gitignore`. Use env vars for all credentials; never hardcode or commit them.
294
+ - **In code**: Read `process.env.CLOUDINARY_API_SECRET` and `process.env.CLOUDINARY_API_KEY` only in server/API code. Never in React components or any file Vite bundles.
295
+ - **Next.js**: `CLOUDINARY_*` in root `.env.local` is server-only (browser only sees `NEXT_PUBLIC_*`). For Vite + Node in same repo, prefer `server/.env` and load it only in the server.
296
+ - **Server SDK**: Use the **Cloudinary Node.js SDK v2** for server-side signing: `import { v2 as cloudinary } from 'cloudinary'` (package name: `cloudinary`). Do **not** use v1 (e.g. 1.47.0) — v1 does not expose `cloudinary.utils.api_sign_request` the same way. Install: `npm install cloudinary` (v2).
297
+
298
+ ### How the client gets credentials (working pattern — use this)
299
+
300
+ Use **`uploadSignature` as a function** (not `signatureEndpoint`). The widget calls the function with `params_to_sign`; your function calls your backend and passes the signature back. This pattern is reliable across widget versions.
301
+
302
+ 1. **Fetch `api_key` from server first** (before creating the widget). API key is not secret; safe to use in client. Your backend returns it from the sign endpoint (e.g. `/api/sign-image`).
303
+
304
+ 2. **Set `uploadSignature` to a function** that receives `(callback, params_to_sign)` from the widget. Inside the function, add `upload_preset` to `params_to_sign` (use your signed preset name, e.g. from env or a constant), POST to your backend with `{ params_to_sign: paramsWithPreset }`, then call `callback(data.signature)` with the response.
305
+
306
+ 3. **Include `uploadPreset` in the widget config** (your signed preset name). The widget needs it so it includes it in `params_to_sign`. **Default:** Cloudinary accounts have a built-in signed preset `ml_default` (users can delete it). If the user has not created their own signed preset, use `ml_default`; otherwise use the preset name from their dashboard.
307
+
308
+ 4. **Server endpoint**: Accept `params_to_sign` from the request body. Always include `upload_preset` in the object you sign (add it if the client did not send it). Use `cloudinary.utils.api_sign_request(paramsToSign, process.env.CLOUDINARY_API_SECRET)` to generate the signature. Return `{ signature, timestamp, api_key, cloud_name }`. Never return the API secret.
309
+
310
+ **Preset name:** Use `ml_default` when the user has not specified a signed preset (Cloudinary provides it by default; users can delete it — then they must create one in the dashboard). Otherwise use the user's preset name.
311
+
312
+ **Generic client pattern** (preset: use `ml_default` if it exists / user hasn't specified one; endpoint is up to the user):
313
+ ```tsx
314
+ // Fetch api_key from server first, then:
315
+ widgetConfig.api_key = data.api_key; // from your sign endpoint
316
+ widgetConfig.uploadPreset = 'ml_default'; // default signed preset (or user's preset if they created one)
317
+ widgetConfig.uploadSignature = function(callback, params_to_sign) {
318
+ const paramsWithPreset = { ...params_to_sign, upload_preset: 'ml_default' };
319
+ fetch('/api/sign-image', {
320
+ method: 'POST',
321
+ headers: { 'Content-Type': 'application/json' },
322
+ body: JSON.stringify({ params_to_sign: paramsWithPreset }),
323
+ })
324
+ .then(r => r.json())
325
+ .then(data => data.signature ? callback(data.signature) : callback(''))
326
+ .catch(() => callback(''));
327
+ };
328
+ ```
329
+
330
+ **Generic server pattern** (Node/Express with SDK v2):
331
+ ```ts
332
+ // import { v2 as cloudinary } from 'cloudinary';
333
+ const params = req.body.params_to_sign || {};
334
+ const paramsToSign = { ...params, upload_preset: params.upload_preset || 'ml_default' };
335
+ const signature = cloudinary.utils.api_sign_request(paramsToSign, process.env.CLOUDINARY_API_SECRET);
336
+ res.json({ signature, timestamp: paramsToSign.timestamp, api_key: process.env.CLOUDINARY_API_KEY, cloud_name: process.env.CLOUDINARY_CLOUD_NAME });
337
+ ```
338
+
339
+ - ❌ **Avoid `signatureEndpoint`** — it may not be called reliably by all widget versions. Prefer the `uploadSignature` function.
340
+ - ✅ Docs: [Upload widget — signed uploads](https://cloudinary.com/documentation/upload_widget#signed_uploads), [Upload assets in Next.js](https://cloudinary.com/documentation/upload_assets_in_nextjs_tutorial).
341
+
342
+ ### Rules for secure uploads
343
+ - ✅ Use a **signed** upload preset (dashboard → Upload presets → Signed). Do not use an unsigned preset when the user wants secure uploads. **Default:** Accounts have a built-in signed preset `ml_default` — use it if the user hasn't created their own (they can delete `ml_default`, in which case they must create a signed preset in the dashboard).
344
+ - ✅ Generate the signature **on the server only** using Cloudinary Node.js SDK **v2** (`cloudinary.utils.api_sign_request`). Never put `CLOUDINARY_API_SECRET` in a `VITE_` variable or in client-side code.
345
+ - ✅ Keep `server/.env` in `.gitignore`; never commit API key or secret.
346
+ - ✅ Use **`uploadSignature` as a function** (not `signatureEndpoint`) for reliable signed uploads.
347
+ - ✅ Include **`uploadPreset` in the widget config** so the widget includes it in `params_to_sign`.
348
+ - ✅ **Server must include `upload_preset` in the signed params** (add it if the client did not send it).
349
+
350
+ ### What not to do
351
+ - ❌ **Never** put the API secret in a `VITE_` (or `NEXT_PUBLIC_`) variable or in any file sent to the browser.
352
+ - ❌ **Never** commit the API key or secret; use env vars and ignore `server/.env` in git.
353
+ - ❌ **Do not** generate the signature in client-side JavaScript (it would require the secret in the client).
354
+ - ❌ **Do not** use an unsigned preset when the user explicitly wants secure/signed uploads.
355
+ - ❌ **Do not** omit `uploadPreset` from the widget config when using signed uploads (widget needs it in `params_to_sign`).
356
+ - ❌ **Do not** use Cloudinary Node SDK v1 (e.g. 1.47.0) for signing — use v2 (`import { v2 as cloudinary } from 'cloudinary'`).
357
+ - ❌ **Do not** rely on `signatureEndpoint` alone; use the `uploadSignature` function for reliability.
358
+
135
359
  ## Video Patterns
136
360
 
361
+ - ✅ **Display a video** → Use **AdvancedVideo** (`@cloudinary/react`). It just displays a video (with optional transformations). Not a full player.
362
+ - ✅ **A video player** → Use **Cloudinary Video Player** (`cloudinary-video-player`). That is the actual player (styled UI, controls, playlists, etc.).
363
+
137
364
  ### ⚠️ IMPORTANT: Two Different Approaches
138
365
 
139
- **1. AdvancedVideo Component** (`@cloudinary/react`) - For video transformations
140
- - React component similar to `AdvancedImage`
141
- - Use for displaying videos with Cloudinary transformations
142
- - Works with `cld.video()` like images work with `cld.image()`
143
- - Simple, React-friendly approach
366
+ **1. AdvancedVideo** (`@cloudinary/react`) For **displaying** a video
367
+ - React component similar to `AdvancedImage`; just displays a video with Cloudinary transformations
368
+ - Not a full "player" — it's video display (native HTML5 video with optional controls)
369
+ - Use when: user wants to show/display a video. Works with `cld.video()` like images with `cld.image()`
144
370
 
145
- **2. Cloudinary Video Player** (`cloudinary-video-player`) - For advanced player features
146
- - Standalone video player library
147
- - Use for advanced features: playlists, recommendations, ads, chapters, etc.
148
- - Full-featured player with analytics, monetization, etc.
149
- - More complex setup, but more powerful
371
+ **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
150
375
 
151
- ### AdvancedVideo Component (React SDK - For Transformations)
152
- - ✅ **Purpose**: Display videos with Cloudinary transformations (resize, effects, etc.)
376
+ ### AdvancedVideo (React SDK - For Displaying a Video)
377
+ - ✅ **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.
153
378
  - ✅ **Package**: `@cloudinary/react` (same as AdvancedImage)
154
379
  - ✅ **Import**: `import { AdvancedVideo } from '@cloudinary/react'`
155
380
  - ✅ **NO CSS IMPORT NEEDED**: AdvancedVideo uses native HTML5 video - no CSS import required
@@ -172,37 +397,49 @@
172
397
  ```
173
398
  - ✅ **Documentation**: https://cloudinary.com/documentation/react_video_transformations
174
399
 
175
- ### Cloudinary Video Player (Standalone - For Advanced Features)
176
- - ✅ **Purpose**: Full-featured video player with playlists, recommendations, ads, etc.
400
+ ### 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.
177
402
  - ✅ **Package**: `cloudinary-video-player` (separate package)
178
- - ✅ **Import**: `import cloudinary from 'cloudinary-video-player'`
179
- - **Import CSS**: `import 'cloudinary-video-player/dist/cld-video-player.css'`
180
- - ✅ **Import modules** (for advanced features):
181
- ```tsx
182
- import 'cloudinary-video-player/dist/adaptive-streaming'; // HLS/DASH
183
- import 'cloudinary-video-player/dist/playlist'; // Playlists
184
- import 'cloudinary-video-player/dist/recommendations-overlay'; // Recommendations
185
- // Or import all: import 'cloudinary-video-player/dist/all'
186
- ```
187
- - ✅ **Initialize in useEffect** with cleanup:
403
+ - ✅ **Import** (named, not default): `import { videoPlayer } from 'cloudinary-video-player'` and `import 'cloudinary-video-player/cld-video-player.min.css'`. If CSS fails, try `'cloudinary-video-player/dist/cld-video-player.css'`.
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):
188
413
  ```tsx
414
+ const videoRef = useRef<HTMLVideoElement>(null);
415
+ const playerRef = useRef<ReturnType<typeof videoPlayer> | null>(null);
189
416
  useEffect(() => {
190
- const player = cloudinary.videoPlayer(ref.current, {
191
- cloud_name: import.meta.env.VITE_CLOUDINARY_CLOUD_NAME,
192
- secure: true,
193
- controls: true,
194
- });
195
- player.source(publicId);
196
- return () => player.dispose(); // Always cleanup!
197
- }, [publicId]);
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" />;
198
436
  ```
199
- - ✅ **Video element classes**: `className="cld-video-player cld-fluid"`
200
437
  - ✅ **Documentation**: https://cloudinary.com/documentation/cloudinary_video_player
201
438
  - ✅ **React Tutorial**: https://cloudinary.com/documentation/video_player_react_tutorial#banner
202
439
 
203
440
  ### When to Use Which?
204
- - ✅ **Use AdvancedVideo** when: You need simple video playback with transformations
205
- - ✅ **Use Video Player** when: You need playlists, recommendations, ads, chapters, or other advanced features
441
+ - ✅ **Use AdvancedVideo** when: User wants to **display** or **show** a video (no full player). It just displays a video with transformations.
442
+ - ✅ **Use Cloudinary Video Player** when: User asks for a **video player** — the actual player with styled UI, controls, and optional features (playlists, ads, etc.).
206
443
 
207
444
  ## TypeScript Patterns
208
445
 
@@ -328,6 +565,10 @@
328
565
 
329
566
  ## Environment Variable Errors
330
567
 
568
+ ### "Where do I create the Cloudinary instance?" / "Config with Vite prefix"
569
+ - ❌ Problem: User is using rules only (no CLI) and doesn't have a Cloudinary config file, or is using wrong env (e.g. process.env, or missing VITE_ prefix).
570
+ - ✅ Create a **single config file** (e.g. `src/cloudinary/config.ts`) that: imports `Cloudinary` from `@cloudinary/url-gen`, reads `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME`, validates it, creates `export const cld = new Cloudinary({ cloud: { cloudName } })`, and exports `uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || ''`. Use **VITE_** prefix in `.env`; access with `import.meta.env.VITE_*` only. See PATTERNS → "Project setup (rules-only / without CLI)" for exact code.
571
+
331
572
  ### "Cloud name is required"
332
573
  - ❌ Problem: `VITE_CLOUDINARY_CLOUD_NAME` not set or wrong prefix
333
574
  - ✅ Solution:
@@ -336,20 +577,18 @@
336
577
  3. Restart dev server after adding .env variables
337
578
 
338
579
  ### "VITE_ prefix required" or env var is undefined
339
- - ❌ Problem: Variable doesn't have `VITE_` prefix
340
- - ✅ Solution:
341
- 1. Rename `CLOUDINARY_CLOUD_NAME` → `VITE_CLOUDINARY_CLOUD_NAME`
342
- 2. Restart dev server
343
- 3. Use `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` (not `process.env`)
580
+ - ❌ Problem: Variable doesn't have the right prefix for the bundler, or wrong access (e.g. process.env in Vite, or no prefix in CRA).
581
+ - ✅ **Vite**: Use `VITE_` prefix in `.env` and `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` (not `process.env`).
582
+ - **Not Vite?** Use your bundler's client env prefix and access: Create React App → `REACT_APP_` and `process.env.REACT_APP_CLOUDINARY_CLOUD_NAME`; Next.js (client) → `NEXT_PUBLIC_` and `process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME`. See PATTERNS → "Other bundlers (non-Vite)".
583
+ - Restart dev server after changing `.env`.
344
584
 
345
585
  ## Import Errors
346
586
 
347
587
  ### "Cannot find module" or wrong import
348
- - ❌ Problem: Importing from wrong package
349
- - ✅ Solution:
350
- - Components: `@cloudinary/react` (not `@cloudinary/url-gen`)
351
- - Transformations: `@cloudinary/url-gen/actions/*` (not `@cloudinary/react`)
352
- - Cloudinary class: `@cloudinary/url-gen` (not `@cloudinary/react`)
588
+ - ❌ Problem: Importing from wrong package or wrong subpath; agent often invents paths that don't exist.
589
+ - ✅ **Use only the exact paths** in PATTERNS → "Import reference: @cloudinary/url-gen (use these exact paths only)". Do **not** guess subpaths (e.g. `@cloudinary/url-gen/resize` or `@cloudinary/url-gen/overlay` — use `actions/resize`, `actions/overlay` with the exact export names from the table).
590
+ - Components and plugins: `@cloudinary/react` (not `@cloudinary/url-gen`). Cloudinary instance and transformation actions/qualifiers: `@cloudinary/url-gen` with the exact subpaths from the Import reference (e.g. `actions/resize`, `actions/delivery`, `qualifiers/format`, `qualifiers/quality`, `actions/overlay`, `qualifiers/gravity`, `qualifiers/textStyle`, `qualifiers/position`, `transformation/Transformation`).
591
+ - If a path fails, check package.json has `@cloudinary/url-gen` and match the import to the Import reference table exactly.
353
592
 
354
593
  ## Transformation Errors
355
594
 
@@ -388,6 +627,15 @@
388
627
 
389
628
  ## Upload Widget Errors
390
629
 
630
+ ### Upload fails (unsigned uploads) — first check upload preset
631
+ - ❌ Problem: Upload fails when using unsigned upload
632
+ - ✅ **Debug checklist** (in order):
633
+ 1. **Is the upload preset configured?** Check `.env` has `VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name` (exact name, no typos)
634
+ 2. **Does the preset exist?** Cloudinary dashboard → Settings → Upload → Upload presets
635
+ 3. **Is it Unsigned?** Preset must be "Unsigned" for client-side uploads (no API key/secret in browser)
636
+ 4. **Env reloaded?** Restart the dev server after any `.env` change
637
+ - ✅ If all above are correct, then check: script loaded in `index.html`, cloud name set, and network/console for the actual error message
638
+
391
639
  ### "Upload preset not found" or "Invalid upload preset"
392
640
  - ❌ Problem: Preset doesn't exist or is signed
393
641
  - ✅ Solution:
@@ -412,6 +660,32 @@
412
660
  2. Check widget initializes in `useEffect` after `window.cloudinary` is available
413
661
  3. Verify upload preset is set correctly
414
662
 
663
+ ### "createUploadWidget is not a function"
664
+ - ❌ 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.
665
+ - ✅ **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.
666
+ - ✅ See PATTERNS → Upload Widget Pattern ("Race condition") and Project setup → Upload Widget ("Wait for script").
667
+
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)`.
672
+
673
+ ### User needs secure/signed uploads
674
+ - ❌ Problem: User asks for secure uploads; unsigned preset or client-side secret is not acceptable.
675
+ - ✅ Use signed preset + server-side signature. Use **`uploadSignature` as a function** (not `signatureEndpoint`); fetch `api_key` from server first; include `uploadPreset` in widget config; server must include `upload_preset` in signed params. Use Cloudinary Node SDK **v2** on server. Never expose or commit the API secret.
676
+ - ✅ See PATTERNS → "Signed vs unsigned uploads" and "Secure (Signed) Uploads" → "How the client gets credentials (working pattern)".
677
+
678
+ ### "Where do I put my API key and secret?" / "Never commit API key or secret"
679
+ - ❌ Problem: User needs to store `CLOUDINARY_API_KEY` and `CLOUDINARY_API_SECRET` securely, or is told to "create a .env file" and worries it will overwrite the existing Vite `.env`.
680
+ - ✅ Do not put them in root `.env`. Create `server/.env` with `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET`; add `server/.env` to `.gitignore`; load only in the server. Never commit API key or secret.
681
+ - ✅ See PATTERNS → "Secure (Signed) Uploads" → "Where to put API key and secret (server-only, never committed)".
682
+
683
+ ### "Invalid Signature" or "Missing required parameter - api_key"
684
+ - ❌ Problem: Signed upload fails with "Invalid Signature" or "Missing required parameter - api_key".
685
+ - ✅ **Use the working pattern:** (1) Use **`uploadSignature` as a function** (not `signatureEndpoint`). (2) **Fetch `api_key` from server** before creating the widget (API key is not secret). (3) **Include `uploadPreset` in widget config** so the widget includes it in `params_to_sign`. (4) **Server must include `upload_preset` in the signed params** (add it if the client did not send it). (5) Use **Cloudinary Node.js SDK v2** on the server (`import { v2 as cloudinary } from 'cloudinary'`), not v1 (e.g. 1.47.0).
686
+ - ✅ **Common mistakes:** Using `signatureEndpoint` instead of `uploadSignature` function; omitting `uploadPreset` from widget config; server not adding `upload_preset` to signature params; using SDK v1 for signing; not fetching `api_key` from server before creating the widget. If using `ml_default`, ensure it still exists (user may have deleted it); otherwise create a signed preset in the dashboard.
687
+ - ✅ See PATTERNS → "Secure (Signed) Uploads" → "How the client gets credentials (working pattern)".
688
+
415
689
  ## Video Errors
416
690
 
417
691
  ### "AdvancedVideo not working" or "Video not displaying"
@@ -443,21 +717,40 @@
443
717
  6. For advanced features, ensure required modules are imported
444
718
 
445
719
  ### Confusion between AdvancedVideo and Video Player
446
- - WRONG: Using `cloudinary-video-player` when you just need transformations
447
- - CORRECT: Use `AdvancedVideo` from `@cloudinary/react` for simple video playback with transformations
448
- - WRONG: Using `AdvancedVideo` when you need playlists/recommendations/ads
449
- - CORRECT: Use `cloudinary-video-player` for advanced features like playlists, recommendations, ads
720
+ - **AdvancedVideo** = for **displaying** a video (not a full player). **Cloudinary Video Player** = the **player** (styled UI, controls, playlists, etc.).
721
+ - WRONG: Using `cloudinary-video-player` when you just need to display a video
722
+ - CORRECT: Use `AdvancedVideo` when you need to display/show a video
723
+ - WRONG: Using `AdvancedVideo` when the user asks for a "video player"
724
+ - ✅ CORRECT: Use `cloudinary-video-player` when they want a video player (or playlists, ads, etc.)
450
725
 
451
726
  ### Memory leak from video player
452
727
  - ❌ WRONG: Not disposing player in cleanup
453
- - ✅ CORRECT: Always dispose player:
728
+ - ✅ CORRECT: Always dispose in cleanup; wrap in try-catch so disposal errors don't throw:
454
729
  ```tsx
455
- useEffect(() => {
456
- const player = cloudinary.videoPlayer(ref.current, config);
457
- return () => player.dispose(); // Always cleanup!
458
- }, [dependencies]);
730
+ return () => {
731
+ if (playerRef.current) {
732
+ try { playerRef.current.dispose(); } catch (e) { console.warn(e); }
733
+ playerRef.current = null;
734
+ }
735
+ };
459
736
  ```
460
737
 
738
+ ### 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).
741
+
742
+ ### Overlay: "Cannot read properties of undefined" or overlay not showing
743
+ - ❌ Problem: Wrong overlay API usage (Overlay.source, compass constants, .transformation().resize, fontWeight on wrong object).
744
+ - ✅ 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).
745
+
746
+ ### Overlay: wrong import path for `text` or `image`
747
+ - ❌ Problem: Importing `text` or `image` from `@cloudinary/url-gen/actions/overlay` — that module only exports `source`.
748
+ - ✅ **`text` and `image`** come from **`@cloudinary/url-gen/qualifiers/source`**. Use the "Canonical overlay block" in these rules: `import { text, image } from '@cloudinary/url-gen/qualifiers/source';`
749
+
750
+ ### Gallery: sample images not loading or 404s
751
+ - ❌ Problem: Assuming sample public IDs (e.g. from the samples list) always exist; users can delete them.
752
+ - ✅ Assume samples might not exist. Use the sample list from PATTERNS → "Creating Image & Video Instances" (e.g. `samples/cloudinary-icon`, `samples/bike`, `samples/landscapes/beach-boat`, `samples/food/spices`, etc.); use **onError** on `AdvancedImage` to hide or remove failed images; prefer uploaded assets when available. See PATTERNS → Image gallery with lazy loading and responsive.
753
+
461
754
  ## TypeScript Errors
462
755
 
463
756
  ### "TypeScript errors on transformations"
@@ -509,15 +802,26 @@
509
802
  ## Quick Reference Checklist
510
803
 
511
804
  When something isn't working, check:
512
- - [ ] Environment variables have `VITE_` prefix
805
+ - [ ] **Rules-only?** Create config file with reusable `cld` and export `uploadPreset`; use your bundler's client env prefix in .env; create Upload Widget in useEffect with ref (see "Project setup (rules-only / without CLI)").
806
+ - [ ] **Not Vite?** → Use your bundler's env prefix and access (e.g. REACT_APP_ + process.env.REACT_APP_*; NEXT_PUBLIC_ + process.env.NEXT_PUBLIC_*). See "Other bundlers (non-Vite)".
807
+ - [ ] Environment variables use the correct prefix for your bundler (Vite: VITE_; CRA: REACT_APP_; Next client: NEXT_PUBLIC_); **never** put API secret in client-exposed vars
513
808
  - [ ] Dev server was restarted after .env changes
514
- - [ ] Imports are from correct packages
809
+ - [ ] **@cloudinary/url-gen imports?** → Use only the exact paths from PATTERNS → "Import reference" and "Canonical overlay block" (e.g. `text`/`image` from `qualifiers/source`, not `actions/overlay`)
810
+ - [ ] Imports are from correct packages (components/plugins from @cloudinary/react; actions/qualifiers from @cloudinary/url-gen with exact paths)
515
811
  - [ ] Transformations are chained on image instance
516
812
  - [ ] Format/quality use separate `.delivery()` calls
517
813
  - [ ] Plugins are in array format
518
814
  - [ ] Upload widget script is loaded in `index.html`
519
- - [ ] Upload preset is unsigned
520
- - [ ] Video player is disposed in cleanup
815
+ - [ ] **"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
817
+ - [ ] **Upload fails (unsigned)?** → Is `VITE_CLOUDINARY_UPLOAD_PRESET` set? Preset exists and is Unsigned in dashboard?
818
+ - [ ] **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
819
+ - [ ] **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
820
+ - [ ] 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`
822
+ - [ ] **Image overlays?** → Import `source` (not Overlay.source); `compass('south_east')` (strings with underscores); `new Transformation()` inside `.transformation()`; fontWeight on TextStyle, textColor on text source
823
+ - [ ] **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)
521
825
  - [ ] CSS files are imported for video player
522
826
  - [ ] TypeScript types are properly imported
523
827
  - [ ] Upload result types are defined (not using `any`)
@@ -16,6 +16,8 @@ dist-ssr
16
16
  .env
17
17
  .env.local
18
18
  .env.production
19
+ # Server-only env (for secure signed uploads); do not commit API secret
20
+ server/.env
19
21
 
20
22
  # Editor directories and files
21
23
  .vscode/*
@@ -60,16 +60,11 @@ function App() {
60
60
 
61
61
  <div className="ai-prompts-section">
62
62
  <h2>🤖 Try Asking Your AI Assistant</h2>
63
- <p className="prompts-intro">Here are some Cloudinary-related tasks you can try:</p>
63
+ <p className="prompts-intro">Ping your agent with one of these to get started:</p>
64
64
  <ul className="prompts-list">
65
- <li>"Add responsive image transformations with breakpoints"</li>
66
- <li>"Create a video player component with Cloudinary"</li>
67
- <li>"Add image effects like blur, grayscale, or sepia"</li>
68
- <li>"Implement automatic image cropping with face detection"</li>
69
- <li>"Add image overlays with text or logos"</li>
70
- <li>"Create an image gallery with lazy loading"</li>
71
- <li>"Add image optimization with format and quality auto"</li>
72
- <li>"Implement image transformations based on screen size"</li>
65
+ <li>Create an image gallery with lazy loading and responsive</li>
66
+ <li>Create a video player that plays a Cloudinary video</li>
67
+ <li>Add image overlays with text or logos</li>
73
68
  </ul>
74
69
  </div>
75
70
  </main>
@@ -22,24 +22,11 @@ export function UploadWidget({
22
22
  }: UploadWidgetProps) {
23
23
  const widgetRef = useRef<any>(null);
24
24
  const buttonRef = useRef<HTMLButtonElement>(null);
25
+ const clickHandlerRef = useRef<(() => void) | null>(null);
25
26
 
26
27
  useEffect(() => {
27
- // Load Cloudinary Upload Widget script
28
- if (!window.cloudinary) {
29
- const script = document.createElement('script');
30
- script.src = 'https://upload-widget.cloudinary.com/global/all.js';
31
- script.async = true;
32
- document.body.appendChild(script);
33
-
34
- script.onload = () => {
35
- initializeWidget();
36
- };
37
- } else {
38
- initializeWidget();
39
- }
40
-
41
28
  function initializeWidget() {
42
- if (!window.cloudinary || !buttonRef.current) return;
29
+ if (typeof window.cloudinary?.createUploadWidget !== 'function' || !buttonRef.current) return;
43
30
 
44
31
  if (!uploadPreset) {
45
32
  console.warn(
@@ -69,16 +56,45 @@ export function UploadWidget({
69
56
  }
70
57
  );
71
58
 
72
- buttonRef.current.addEventListener('click', () => {
73
- widgetRef.current?.open();
74
- });
59
+ const handler = () => widgetRef.current?.open();
60
+ clickHandlerRef.current = handler;
61
+ buttonRef.current.addEventListener('click', handler);
62
+ }
63
+
64
+ function waitThenInit() {
65
+ if (typeof window.cloudinary?.createUploadWidget === 'function') {
66
+ initializeWidget();
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ if (!window.cloudinary) {
73
+ const script = document.createElement('script');
74
+ script.src = 'https://upload-widget.cloudinary.com/global/all.js';
75
+ script.async = true;
76
+ document.body.appendChild(script);
77
+ script.onload = () => waitThenInit();
78
+ } else if (!waitThenInit()) {
79
+ const poll = setInterval(() => {
80
+ if (waitThenInit()) {
81
+ clearInterval(poll);
82
+ clearTimeout(timeout);
83
+ }
84
+ }, 100);
85
+ const timeout = setTimeout(() => clearInterval(poll), 10000);
86
+ return () => {
87
+ clearInterval(poll);
88
+ clearTimeout(timeout);
89
+ if (buttonRef.current && clickHandlerRef.current) {
90
+ buttonRef.current.removeEventListener('click', clickHandlerRef.current);
91
+ }
92
+ };
75
93
  }
76
94
 
77
95
  return () => {
78
- if (buttonRef.current && widgetRef.current) {
79
- buttonRef.current.removeEventListener('click', () => {
80
- widgetRef.current?.open();
81
- });
96
+ if (buttonRef.current && clickHandlerRef.current) {
97
+ buttonRef.current.removeEventListener('click', clickHandlerRef.current);
82
98
  }
83
99
  };
84
100
  }, [onUploadSuccess, onUploadError]);