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.
- package/.github/workflows/release.yml +2 -2
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/templates/.cursorrules.template +373 -69
- package/templates/.gitignore.template +2 -0
- package/templates/src/App.tsx.template +4 -9
- package/templates/src/cloudinary/UploadWidget.tsx.template +38 -22
|
@@ -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
|
|
175
|
-
npm publish --provenance --access public --tag
|
|
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,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
|
|
19
|
-
- ✅ CORRECT: `VITE_CLOUDINARY_CLOUD_NAME=mycloud` in `.env`
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
140
|
-
- React component similar to `AdvancedImage
|
|
141
|
-
-
|
|
142
|
-
- Works with `cld.video()` like images
|
|
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`)
|
|
146
|
-
-
|
|
147
|
-
- Use for
|
|
148
|
-
-
|
|
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
|
|
152
|
-
- ✅ **Purpose**: Display
|
|
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 (
|
|
176
|
-
- ✅ **Purpose**:
|
|
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
|
|
179
|
-
-
|
|
180
|
-
- ✅ **
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
- ✅ **
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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:
|
|
205
|
-
- ✅ **Use Video Player** when:
|
|
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
|
|
340
|
-
- ✅
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
- ✅
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
-
|
|
447
|
-
-
|
|
448
|
-
-
|
|
449
|
-
-
|
|
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
|
|
728
|
+
- ✅ CORRECT: Always dispose in cleanup; wrap in try-catch so disposal errors don't throw:
|
|
454
729
|
```tsx
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
- [ ]
|
|
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
|
-
- [ ]
|
|
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
|
-
- [ ]
|
|
520
|
-
- [ ] Video player
|
|
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`)
|
|
@@ -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">
|
|
63
|
+
<p className="prompts-intro">Ping your agent with one of these to get started:</p>
|
|
64
64
|
<ul className="prompts-list">
|
|
65
|
-
<li>
|
|
66
|
-
<li>
|
|
67
|
-
<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 (
|
|
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
|
-
|
|
73
|
-
|
|
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 &&
|
|
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]);
|