diff --git a/cli.js b/cli.js index 23597f1..707cfbf 100755 --- a/cli.js +++ b/cli.js @@ -3,7 +3,7 @@ import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; -import { spawnSync } from 'child_process'; +import { spawnSync, spawn } from 'child_process'; import inquirer from 'inquirer'; import chalk from 'chalk'; import fs from 'fs-extra'; @@ -208,12 +208,14 @@ async function main() { name: 'aiTools', message: 'Which AI coding assistant(s) are you using? (Select all that apply)\n' + - chalk.gray(' We’ll add local instruction files so your assistant knows Cloudinary patterns.\n'), + chalk.gray(' We\'ll install Cloudinary skills in the right location for each tool.\n'), choices: [ { name: 'Cursor', value: 'cursor' }, - { name: 'GitHub Copilot', value: 'copilot' }, { name: 'Claude Code', value: 'claude' }, - { name: 'Other / Generic AI tools', value: 'generic' }, + { name: 'GitHub Copilot', value: 'copilot' }, + { name: 'OpenAI Codex', value: 'codex' }, + { name: 'Gemini CLI', value: 'gemini' }, + { name: 'Other', value: 'generic' }, ], default: ['cursor'], }, @@ -316,35 +318,68 @@ async function main() { copyTemplate(file); }); - // Create AI rules based on user's tool selection - const aiRulesTemplatePath = join(TEMPLATES_DIR, '.cursorrules.template'); - if (existsSync(aiRulesTemplatePath) && aiTools && aiTools.length > 0) { - const aiRulesContent = replaceTemplate( - readFileSync(aiRulesTemplatePath, 'utf-8'), - templateVars - ); + // Install Cloudinary skills into the directories each selected tool actually reads: + // .cursor/skills/ = Cursor + // .claude/skills/ = Claude Code + // .agents/skills/ = Copilot, Codex, Gemini, Generic (and any unrecognised tool) + console.log(chalk.blue('\nπŸ€– Installing Cloudinary AI skills...\n')); + + const skillTargetDirs = new Set(); + if (aiTools && aiTools.includes('cursor')) skillTargetDirs.add(join(projectPath, '.cursor', 'skills')); + if (aiTools && aiTools.includes('claude')) skillTargetDirs.add(join(projectPath, '.claude', 'skills')); + if (!aiTools || aiTools.some(t => !['cursor', 'claude'].includes(t))) { + skillTargetDirs.add(join(projectPath, '.agents', 'skills')); + } - // Generate files based on selected tools - if (aiTools.includes('cursor')) { - writeFileSync(join(projectPath, '.cursorrules'), aiRulesContent); - } + const skills = ['cloudinary-docs', 'cloudinary-react', 'cloudinary-transformations']; + // Download each skill once then copy to any additional targets + const [primaryDir, ...additionalDirs] = skillTargetDirs; + mkdirSync(primaryDir, { recursive: true }); - if (aiTools.includes('copilot')) { - const githubDir = join(projectPath, '.github'); - mkdirSync(githubDir, { recursive: true }); - writeFileSync(join(githubDir, 'copilot-instructions.md'), aiRulesContent); - } + // Print all skill names upfront so the user sees activity immediately + for (const skill of skills) { + console.log(chalk.gray(` Fetching ${skill}...`)); + } - if (aiTools.includes('claude')) { - writeFileSync(join(projectPath, 'CLAUDE.md'), aiRulesContent); + // Download all 3 skills in parallel + const results = await Promise.all( + skills.map(skill => new Promise(resolve => { + const proc = spawn( + 'npx', + ['--yes', 'degit', `cloudinary-devs/skills/skills/${skill}`, join(primaryDir, skill)], + { stdio: 'pipe', shell: false } + ); + proc.on('close', code => resolve({ skill, ok: code === 0 })); + })) + ); + + const BUNDLED_SKILLS_DIR = join(__dirname, 'skills'); + + let skillsInstalled = 0; + for (const { skill, ok } of results) { + if (ok) { + console.log(chalk.gray(` βœ“ ${skill}`)); + skillsInstalled++; + } else { + const bundled = join(BUNDLED_SKILLS_DIR, skill); + if (existsSync(bundled)) { + fs.copySync(bundled, join(primaryDir, skill)); + console.log(chalk.gray(` βœ“ ${skill} (installed from local cache)`)); + skillsInstalled++; + } else { + console.warn(chalk.yellow(` ⚠ Could not install ${skill}`)); + } } + } - if (aiTools.includes('generic')) { - writeFileSync(join(projectPath, 'AI_INSTRUCTIONS.md'), aiRulesContent); - writeFileSync(join(projectPath, 'PROMPT.md'), aiRulesContent); + if (skillsInstalled > 0) { + for (const target of additionalDirs) { + fs.copySync(primaryDir, target); } + } - // Generate MCP configuration: Cursor uses .cursor/mcp.json, Claude Code uses .mcp.json in project root + // Generate MCP configuration: Cursor uses .cursor/mcp.json, Claude Code uses .mcp.json in project root + if (aiTools && aiTools.length > 0) { const mcpTemplatePath = join(TEMPLATES_DIR, '.cursor/mcp.json.template'); if (existsSync(mcpTemplatePath)) { const mcpContent = replaceTemplate( @@ -370,26 +405,16 @@ async function main() { console.log(chalk.green('βœ… Project created successfully!\n')); - if (aiTools && aiTools.length > 0) { - // Count actual files created - let fileCount = 0; - if (aiTools.includes('cursor')) fileCount += 2; // .cursorrules + mcp.json - if (aiTools.includes('copilot')) fileCount += 1; - if (aiTools.includes('claude')) fileCount += 2; // CLAUDE.md + .mcp.json - if (aiTools.includes('generic')) fileCount += 2; // AI_INSTRUCTIONS.md + PROMPT.md - - const filesText = fileCount === 1 ? 'file' : 'files'; - console.log(chalk.cyan(`πŸ“‹ AI assistant configuration ${filesText} created:`)); - if (aiTools.includes('cursor')) console.log(chalk.gray(' β€’ Cursor: .cursorrules')); - if (aiTools.includes('copilot')) console.log(chalk.gray(' β€’ GitHub Copilot: .github/copilot-instructions.md')); - if (aiTools.includes('claude')) console.log(chalk.gray(' β€’ Claude: CLAUDE.md')); - if (aiTools.includes('generic')) console.log(chalk.gray(' β€’ Generic: AI_INSTRUCTIONS.md, PROMPT.md')); - if (aiTools.includes('cursor')) console.log(chalk.gray(' β€’ MCP (Cursor): .cursor/mcp.json')); - if (aiTools.includes('claude')) console.log(chalk.gray(' β€’ MCP (Claude Code): .mcp.json')); - console.log(chalk.gray(`\n ${fileCount === 1 ? 'This file teaches' : 'These files teach'} your AI assistant about Cloudinary patterns and best practices.`)); - console.log(chalk.gray(`\n πŸ’‘ How to use ${fileCount === 1 ? 'this file' : 'these files'}:`)); - console.log(chalk.gray(' β€’ Simply open your project in your AI assistant - the configuration is already loaded')); - console.log(chalk.gray(' β€’ Ask your AI to help build Cloudinary features, and it will follow these patterns')); + if (skillsInstalled > 0) { + console.log(chalk.cyan(`\nπŸ“‹ AI skills installed β†’ .agents/skills/ (works with Cursor, Claude Code, Copilot, and more):`)); + console.log(chalk.gray(' β€’ cloudinary-docs β€” looks up live Cloudinary documentation')); + console.log(chalk.gray(' β€’ cloudinary-react β€” uses React SDK patterns and best practices')); + console.log(chalk.gray(' β€’ cloudinary-transformations β€” creates and debugs Cloudinary transformation URLs from natural language instructions')); + if (aiTools && aiTools.includes('cursor')) console.log(chalk.gray(' β€’ MCP (Cursor): .cursor/mcp.json')); + if (aiTools && aiTools.includes('claude')) console.log(chalk.gray(' β€’ MCP (Claude Code): .mcp.json')); + console.log(chalk.gray('\n πŸ’‘ How to use:')); + console.log(chalk.gray(' β€’ Open your project in your AI assistant β€” skills are picked up automatically')); + console.log(chalk.gray(' β€’ Ask your AI assistant to help build Cloudinary features and it will use these skills')); console.log(chalk.gray(' β€’ Example prompts: "Add image upload", "Create a transformation gallery"\n')); } diff --git a/skills/cloudinary-docs/SKILL.md b/skills/cloudinary-docs/SKILL.md new file mode 100644 index 0000000..a9690f2 --- /dev/null +++ b/skills/cloudinary-docs/SKILL.md @@ -0,0 +1,53 @@ +--- +name: cloudinary-docs +description: Local version -- Looks up implementation details in the latest Cloudinary docs via llms.txt. Use when building code or answering questions relating to image or video uploads, optimization, or transformations, and for Cloudinary SDKs, APIs, webhooks, or integrations. +license: MIT +metadata: + author: cloudinary + version: '1.0.1' +--- + +# Cloudinary Documentation + +Helps developers integrate Cloudinary into their applications by providing documentation and code examples retrieved directly from the optimized markdown files in the Cloudinary documentation. + +## When to Use + +- When a user asks questions or requests code implementation relating to image or video upload, management, optimization, or transformations (resizing, applying effects, visual improvements, adding overlays, generative AI, etc.) +- User asks about Cloudinary SDKs, upload APIs, or integration guides +- General Cloudinary documentation lookup (account settings, webhooks, DAM features) +- Looking up specific Cloudinary API endpoints or SDK methods +- Use this skill in conjunction with more specialized Cloudinary skills when relevant. + +## Instructions + +When answering image and video upload, management, optimization, or transformation questions or when implementing Cloudinary code: + +1. **First, get the documentation index** using llms.txt with the llms.txt URL - https://cloudinary.com/documentation/llms.txt +2. **Analyze the llms.txt content** to understand what documentation pages are available +3. **Reflect on the user's question** and identify which specific documentation URLs would be most relevant +4. **Navigate** to the specific relevant documentation URLs from the llms.txt index (you can make multiple calls) +5. **Use the fetched documentation** to provide a comprehensive, accurate answer or code implementation. When relevant, use in conjunction with more specialized Cloudinary skills like cloudinary-transformations. The best practices defined in the specialized skills should guide which doc instructions to use. + +Example workflows: + +**Example 1: Upload question** +- User asks: "How do I upload images to Cloudinary?" +- You retrieve the llms.txt index: https://cloudinary.com/documentation/llms.txt +- You analyze the llms.txt content to understand what documentation pages are available +- You identify relevant pages like "image_upload.md" or "upload_api.md" +- You retrieve those specific pages from the llms.txt index +- You provide an answer with code examples and citations + +**Example 2: Transformation question** +- User asks: "How do I resize and crop images?" +- You retrieve the llms.txt index +- You identify relevant pages like "image_transformations.md" or "transformation_reference.md" +- You fetch the specific documentation +- You provide transformation syntax and examples + +**Example 3: SDK question** +- User asks: "What's the Node.js SDK for Cloudinary?" +- You retrieve the llms.txt index +- You identify SDK-related pages +- You provide installation instructions and usage examples \ No newline at end of file diff --git a/skills/cloudinary-react/SKILL.md b/skills/cloudinary-react/SKILL.md new file mode 100644 index 0000000..0a34687 --- /dev/null +++ b/skills/cloudinary-react/SKILL.md @@ -0,0 +1,486 @@ +--- +name: cloudinary-react +description: Local version -- Provides opinionated React SDK patterns for configuration, common integration scenarios, and troubleshooting for frequent errors and TypeScript pitfalls. Use when writing code or answering questions related to the Cloudinary React SDK. +license: MIT +metadata: + author: cloudinary + version: '1.0.1' +--- + +# Cloudinary React Skill + +## When to Use + +- When a user is building or debugging Cloudinary in a **React** app (Vite, Create React App, Parcel, etc.). +- When implementing or fixing: Upload Widget, AdvancedImage/AdvancedVideo, transformations, overlays, image galleries, video player, or signed/unsigned uploads. +- When the user sees errors like "createUploadWidget is not a function", wrong imports from `@cloudinary/url-gen`, upload preset issues, or video player DOM errors. + + +## Quick Start + +**Most common operations:** +1. **Setup**: Create config file with `cld` instance (see Project setup section) +2. **Display image**: `const img = cld.image('id').resize(fill().width(800)); ` +3. **Upload Widget**: Script in index.html + poll for `createUploadWidget` in useEffect +4. **Image overlay**: Use `source(text(...))` or `source(image(...))` - see Import reference table for exact paths +5. **Signed uploads**: See [references/signed-uploads.md](references/signed-uploads.md) +6. **Troubleshooting**: See [references/troubleshooting.md](references/troubleshooting.md) + +**For TypeScript**: See [references/typescript-patterns.md](references/typescript-patterns.md) +**For Video Player**: See [references/video-player.md](references/video-player.md) + + + +## Instructions + +When helping with Cloudinary in React, follow the patterns and rules below. Use the **exact** import paths and code patterns specified; do not guess subpaths or invent APIs. + +--- + +# Cloudinary React SDK Patterns & Common Errors + +**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. + +## Official Documentation + +**For complete API references and all configuration options**, consult the Cloudinary documentation via **llms.txt**: https://cloudinary.com/documentation/llms.txt + +This skill provides React patterns and common errors. For detailed API options (e.g., all Upload Widget config properties, Video Player API methods), use the documentation markdown files available through llms.txt. + +### Key References +- **Transformation Rules**: https://cloudinary.com/documentation/cloudinary_transformation_rules.md +- **Transformation Reference**: https://cloudinary.com/documentation/transformation_reference.md +- **React Image Transformations & Plugins**: https://cloudinary.com/documentation/react_image_transformations.md#plugins +- **React Video Transformations**: https://cloudinary.com/documentation/react_video_transformations.md +- **Upload Widget Reference** (complete config options): https://cloudinary.com/documentation/upload_widget_reference.md +- **Upload Widget (signed uploads)**: https://cloudinary.com/documentation/upload_widget.md#signed_uploads +- **Cloudinary Video Player** (standalone player): https://cloudinary.com/documentation/cloudinary_video_player.md +- **Video Player API Reference** (complete API methods): https://cloudinary.com/documentation/video_player_api_reference.md +- **Video Player React Tutorial**: https://cloudinary.com/documentation/video_player_react_tutorial.md +- **Upload assets in Next.js (backend signature)**: https://cloudinary.com/documentation/upload_assets_in_nextjs_tutorial.md +- **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.md +- **React Native image and video upload (signed)**: https://cloudinary.com/documentation/react_native_image_and_video_upload.md#signed_upload +- Always consult the official transformation rules when creating transformations +- Use only officially supported parameters from the transformation reference + +**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`). + +--- + +# πŸ“‹ PATTERNS (How to Do It Right) + +## Project setup (rules-only / without CLI) + +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. + +**1. Environment (.env)** +Create a `.env` file in the project root with **Vite prefix** (required for client access): +- `VITE_CLOUDINARY_CLOUD_NAME_=my_cloud` (required β€” use your actual cloud name, **never** the literal string `your_cloud_name` which causes 401) +- `VITE_CLOUDINARY_UPLOAD_PRESET=my_preset` (optional; required for unsigned upload widget β€” use your actual preset name) +- **Restart the dev server** after adding or changing `.env`. Use `import.meta.env.VITE_*` in code, not `process.env`. +- **If env var still empty in browser after restart**: Vite may cache the old value. Clear `node_modules/.vite/`, restart dev server, and do a hard refresh (Cmd+Shift+R / Ctrl+Shift+F5). If still empty, see "Vite env not reaching client" in Common Errors. + +**2. Reusable Cloudinary instance (config)** +Create a config file (e.g. `src/cloudinary/config.ts`) so the rest of the app can use a single `cld` instance: +```ts +import { Cloudinary } from '@cloudinary/url-gen'; + +const cloudName = import.meta.env.VITE_CLOUDINARY_CLOUD_NAME; +if (!cloudName) { + throw new Error('VITE_CLOUDINARY_CLOUD_NAME is not set. Add it to .env with the VITE_ prefix.'); +} + +export const cld = new Cloudinary({ cloud: { cloudName } }); +export const uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || ''; +``` +- 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)`. + +**3. Upload Widget (unsigned, from scratch)** + +**Strict pattern (always follow this exactly):** +1. **Script in `index.html`** (required): Add `` to `index.html`. Do **not** rely only on dynamic script injection from React β€” it's fragile. +2. **Poll in useEffect** (required): In `useEffect`, poll with `setInterval` (e.g. every 100ms) until `typeof window.cloudinary?.createUploadWidget === 'function'`. Only then create the widget. A single check (even in `onload`) is **not** reliable because `window.cloudinary` can exist before `createUploadWidget` is attached. +3. **Add a timeout**: Set a timeout (e.g. 10 seconds) to stop polling and show an error if the script never loads. Clear both interval and timeout in cleanup. +4. **Create widget once**: When `createUploadWidget` is available, create the widget and store it in a **ref**. Clear the interval and timeout. Pass options: `{ cloudName, uploadPreset, sources: ['local', 'camera', 'url'], multiple: false }`. +5. **Open on click**: Attach a click listener to a button that calls `widgetRef.current?.open()`. Remove the listener in useEffect cleanup. + +❌ **Do NOT**: Check only `window.cloudinary` (not enough); do a single check in `onload` (unreliable); skip the script in `index.html`; poll forever without a timeout. +- **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). + +**4. Video player** +- Use imperative video element only (create with document.createElement, append to container ref, pass to videoPlayer). See "Cloudinary Video Player (The Player)" for the full pattern. + +**5. Summary for rules-only users** +- **Env**: Use your bundler's client env prefix and access (Vite: `VITE_` + `import.meta.env.VITE_*`; see "Other bundlers" if not Vite). +- **Reusable instance**: One config file that creates and exports `cld` (and optionally `uploadPreset`) from `@cloudinary/url-gen`; use it everywhere. +- **Upload widget**: Script in index.html (required); in useEffect, **poll** until `createUploadWidget` is a function, then create widget once and store in ref; unsigned = cloudName + uploadPreset; signed = use uploadSignature function and backend. +- **Video player**: Imperative video element (createElement, append to container ref, pass to videoPlayer); dispose + removeChild in cleanup; fall back to AdvancedVideo if init fails. + +**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. + +## Environment Variables +- **Default: Vite** β€” Vite requires `VITE_` prefix; use `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` (not `process.env`). Restart dev server after changing `.env`. +- βœ… CORRECT (Vite): `VITE_CLOUDINARY_CLOUD_NAME=mycloud` in `.env`; `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` +- ❌ **WRONG**: `VITE_CLOUDINARY_CLOUD_NAME=your_cloud_name` β€” **Never** use the literal placeholder `your_cloud_name`; it causes 401 errors. Use your actual cloud name from the Cloudinary dashboard. +- ⚠️ **Vite env not reaching client**: If `import.meta.env.VITE_CLOUDINARY_CLOUD_NAME` is still `undefined` in the browser after restarting: + 1. Clear Vite cache: `rm -rf node_modules/.vite/` (or delete `node_modules/.vite` folder) + 2. Restart dev server: `npm run dev` + 3. Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+F5 (Windows/Linux) + 4. If still empty, use a **static config** workaround (create a `cloudinaryConfig.ts` with a hardcoded cloud name for dev, then switch back to env later) and see Common Errors β†’ "VITE_ prefix required or env var is undefined" + +## Other bundlers (non-Vite) +- **Only the env access changes.** All other patterns (reusable `cld`, Upload Widget, Video Player, overlays, signed uploads) are bundler-agnostic. +- **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. +- **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_`. +- **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.*`). +- **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. + +## Upload Presets +- **Unsigned** = client-only uploads (no backend). **Signed** = backend required, more secure. See **"Signed vs unsigned uploads"** below for when to use which. +- βœ… Create unsigned upload preset (for simple client uploads): https://console.cloudinary.com/app/settings/upload/presets +- βœ… Set preset in `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your-preset-name` +- βœ… Use in code: `import { uploadPreset } from './cloudinary/config'` +- ⚠️ If upload preset is missing, the Upload Widget will show an error message +- ⚠️ Upload presets must be set to "Unsigned" mode for client-side usage (no API key/secret needed) +- **When unsigned upload fails**: First check that the user configured their upload preset: + 1. Is `VITE_CLOUDINARY_UPLOAD_PRESET` set in `.env`? (must match preset name exactly) + 2. Does the preset exist in the dashboard under Settings β†’ Upload β†’ Upload presets? + 3. Is the preset set to **Unsigned** (not Signed)? + 4. Was the dev server restarted after adding/updating `.env`? + +## Installing Cloudinary packages +- βœ… **Install the latest**: When adding Cloudinary packages, use `npm install ` **with no version** so npm installs the latest compatible version (e.g. `npm install cloudinary-video-player`). In package.json use a **caret range** (e.g. `"cloudinary-video-player": "^1.0.0"`) so future installs get the latest compatible. Do not pin to an exact version unless you have verified it exists on npm. +- βœ… **Package names only**: Use **only** these names: `@cloudinary/react`, `@cloudinary/url-gen`, `cloudinary-video-player` (standalone player), `cloudinary` (Node server-side only). Do not invent names (e.g. no `@cloudinary/video-player`). +- ❌ **WRONG**: `npm install cloudinary-video-player@1.2.3` or `"cloudinary-video-player": "1.2.3"` (exact pin) β€” versions may not exist and break installs. +- βœ… **Correct**: `npm install cloudinary-video-player` (no version) or in package.json: `"cloudinary-video-player": "^1.0.0"` (caret = latest compatible). + +## Import Patterns +- βœ… Import Cloudinary instance: `import { cld } from './cloudinary/config'` +- βœ… Import components: `import { AdvancedImage, AdvancedVideo } from '@cloudinary/react'` +- βœ… Import plugins: `import { responsive, lazyload, placeholder } from '@cloudinary/react'` +- βœ… **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`). + +## Import reference: @cloudinary/url-gen (use these exact paths only) + +**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". + +| Purpose | Exact import | +|--------|----------------| +| Cloudinary instance (config) | `import { Cloudinary } from '@cloudinary/url-gen';` | +| Resize (fill) | `import { fill } from '@cloudinary/url-gen/actions/resize';` | +| Resize (scale, for overlays) | `import { scale } from '@cloudinary/url-gen/actions/resize';` | +| Delivery format/quality | `import { format, quality } from '@cloudinary/url-gen/actions/delivery';` | +| Format qualifier (auto) | `import { auto } from '@cloudinary/url-gen/qualifiers/format';` | +| Quality qualifier (auto) | `import { auto as autoQuality } from '@cloudinary/url-gen/qualifiers/quality';` | +| Effects (e.g. blur) | `import { blur } from '@cloudinary/url-gen/actions/effect';` | +| Overlay source | `import { source } from '@cloudinary/url-gen/actions/overlay';` | +| Overlay text / image (source types) | `import { text, image } from '@cloudinary/url-gen/qualifiers/source';` | +| Overlay image transformation | `import { Transformation } from '@cloudinary/url-gen/transformation/Transformation';` | +| Position (overlay) | `import { Position } from '@cloudinary/url-gen/qualifiers/position';` | +| Gravity/compass | `import { compass } from '@cloudinary/url-gen/qualifiers/gravity';` | +| Text style (overlay) | `import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle';` | +| Types | `import type { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';` | + +**Canonical overlay block (copy these imports and patterns exactly):** +```ts +// Overlay imports β€” text/image from qualifiers/source, NOT actions/overlay +import { source } from '@cloudinary/url-gen/actions/overlay'; +import { text, image } from '@cloudinary/url-gen/qualifiers/source'; +import { Position } from '@cloudinary/url-gen/qualifiers/position'; +import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle'; +import { compass } from '@cloudinary/url-gen/qualifiers/gravity'; +import { Transformation } from '@cloudinary/url-gen/transformation/Transformation'; +import { scale } from '@cloudinary/url-gen/actions/resize'; + +// Text overlay (compass with underscores: 'south_east', 'center') +cld.image('id').overlay( + source(text('Hello', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white')) + .position(new Position().gravity(compass('center'))) +); + +// Image overlay (logo/image with resize) +cld.image('id').overlay( + source(image('logo').transformation(new Transformation().resize(scale().width(100)))) + .position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20)) +); +``` + +- **Components** (AdvancedImage, AdvancedVideo, plugins) come from **`@cloudinary/react`**, not from `@cloudinary/url-gen`. +- **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. +- 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). + +## Creating Image & Video Instances +- βœ… Create image instance: `const img = cld.image(publicId)` +- βœ… Create video instance: `const video = cld.video(publicId)` (same pattern as images) +- βœ… Public ID format: Use forward slashes for folders (e.g., `'folder/subfolder/image'`) +- βœ… Public IDs are case-sensitive and should not include file extensions +- βœ… **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. +- βœ… **Sample public IDs that may be available** (use for galleries, demos; handle onError if missing): + - 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` + - Video: `samples/elephants` +- βœ… **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. +- βœ… Examples: + ```tsx + const displayImage = cld.image('samples/cloudinary-icon'); + const displayVideo = cld.video('samples/elephants'); + // Gallery: e.g. ['samples/bike', 'samples/landscapes/beach-boat', 'samples/food/spices', ...] + ``` + +## Transformation Patterns + +### Image Transformations +- βœ… Chain transformations on image instance: + ```tsx + const img = cld.image('id') + .resize(fill().width(800).height(600)) + .effect(blur(800)) + .delivery(format(auto())) + .delivery(quality(autoQuality())); + ``` +- βœ… Pass to component: `` + +### Video Transformations +- βœ… Chain transformations on video instance (same pattern as images): + ```tsx + const video = cld.video('id') + .resize(fill().width(800).height(600)) + .delivery(format(auto())); + ``` +- βœ… Pass to component: `` +- βœ… Video transformations work the same way as image transformations + +### Transformation Best Practices +- βœ… Format and quality must use separate `.delivery()` calls +- βœ… Always end with auto format/quality: `.delivery(format(auto())).delivery(quality(autoQuality()))` unless user specifies a particular format or quality +- βœ… Use `gravity(auto())` unless user specifies a focal point +- βœ… Same transformation syntax works for both images and videos + +## Plugin Patterns +- βœ… **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. +- βœ… Import plugins from `@cloudinary/react` +- βœ… Pass plugins as array: `plugins={[responsive(), lazyload(), placeholder()]}` +- βœ… Recommended plugin order: + 1. `responsive()` - First (handles breakpoints) + 2. `placeholder()` - Second (shows placeholder while loading) + 3. `lazyload()` - Third (delays loading until in viewport) + 4. `accessibility()` - Last (if needed) +- βœ… Always add `width` and `height` attributes to prevent layout shift +- βœ… Example: + ```tsx + + ``` + +## Responsive Images Pattern +- βœ… **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"`). +- βœ… Use `responsive()` plugin with `fill()` resize +- βœ… Combine with `placeholder()` and `lazyload()` plugins +- βœ… Example: + ```tsx + const img = cld.image('id').resize(fill().width(800)); + + ``` + +## Image gallery with lazy loading and responsive +- βœ… **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. +- βœ… **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. +- βœ… **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. +- βœ… **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. + +## Image Overlays (text or logos) +- βœ… **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`. +- βœ… **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`. +- βœ… **compass()** takes **string** values, with **underscores**: `compass('center')`, `compass('south_east')`, `compass('north_west')`. ❌ WRONG: `compass(southEast)` or `'southEast'` (no camelCase). +- βœ… **Overlay image**: Use `new Transformation()` **inside** `.transformation()`: `image('logo').transformation(new Transformation().resize(scale().width(100)))`. ❌ WRONG: `image('logo').transformation().resize(...)` (`.transformation()` does not return a chainable object). +- βœ… **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')`. +- βœ… **Position** is chained **after** `source(...)`, not inside: `source(image('logo').transformation(...)).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20))`. +- βœ… **Image overlay pattern**: `baseImage.overlay(source(image('id').transformation(new Transformation().resize(scale().width(100)))).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20)))`. (Import `scale` from `@cloudinary/url-gen/actions/resize` if needed.) +- βœ… **Text overlay pattern**: `baseImage.overlay(source(text('Your Text', new TextStyle('Arial', 60).fontWeight('bold')).textColor('white')).position(new Position().gravity(compass('center'))))`. +- βœ… Docs: React Image Transformations and transformation reference for overlay syntax. + +## Upload Widget Pattern +- βœ… Use component: `import { UploadWidget } from './cloudinary/UploadWidget'` + +**Strict initialization pattern (always follow this exactly):** +1. βœ… **Script in `index.html`** (required): + ```html + + ``` +2. βœ… **Poll in useEffect until `createUploadWidget` is available** (required): Use `setInterval` (e.g. every 100ms) to check `typeof window.cloudinary?.createUploadWidget === 'function'`. Only create the widget when this returns `true`. Clear the interval once ready. +3. βœ… **Add a timeout** (e.g. 10 seconds) to stop polling and show an error state if the script never loads. Clear both interval and timeout in cleanup and when ready. +4. βœ… **Create widget once**, store in a ref. Cleanup: clear interval, clear timeout, remove click listener. + +❌ **Do NOT**: Check only `window.cloudinary` (the function may not be attached yet); do a single check in `onload` (unreliable timing); skip `index.html` and rely only on dynamic injection; poll forever without a timeout. + +- βœ… Create unsigned upload preset in dashboard at `settings/upload/presets` +- βœ… Add to `.env`: `VITE_CLOUDINARY_UPLOAD_PRESET=your_preset_name` +- βœ… Handle callbacks: + ```tsx + { + console.log('Public ID:', result.public_id); + }} + onUploadError={(error) => { + console.error('Upload failed:', error); + }} + /> + ``` +- βœ… Upload result contains: `public_id`, `secure_url`, `width`, `height`, etc. + +**For complete Upload Widget configuration options** (all properties for `createUploadWidget`), see the official reference: https://cloudinary.com/documentation/upload_widget_reference.md + +## Signed vs unsigned uploads (when to use which) + +**Unsigned uploads** (simpler, no backend required): +- Use when: Quick prototypes, low-risk apps, or when anyone with the preset name may upload. +- Preset: Create an **Unsigned** upload preset in Cloudinary dashboard (Settings β†’ Upload β†’ Upload presets). Put preset name in `.env` as `VITE_CLOUDINARY_UPLOAD_PRESET`. +- Client: Widget needs only `cloudName` and `uploadPreset`. No API key or secret; no backend. +- Trade-off: Anyone who knows the preset name can upload. Use only when that is acceptable. + +**Signed uploads** (more secure, backend required): +- Use when: Production apps, authenticated users, or when you need to control who can upload. +- Preset: Create a **Signed** upload preset in the dashboard. The backend generates a signature using your API secret; the client never sees the secret. +- 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. +- Trade-off: Requires a backend (Node/Express, Next.js API route, etc.) to sign requests. More secure; signature validates each upload. + +**Rule of thumb**: **Default to unsigned uploads** unless the user explicitly asks for "secure" or "signed" uploads. Do not default to signed β€” it requires a running backend and will fail out of the box. Use **signed** only when the user explicitly requests secure/signed uploads or needs to restrict who can upload. + +## Secure (Signed) Uploads + +**When to use:** Production apps, authenticated users, or when you need to control who can upload (more secure than unsigned). + +**Golden rules:** +1. Never expose or commit the API secret (server-only) +2. Use `server/.env` in `.gitignore` for API key/secret +3. API key can be sent to client; API secret must stay server-only + +**Quick implementation:** +- Use `uploadSignature` as function (not `signatureEndpoint`) +- Fetch `api_key` from server before creating widget +- Include `uploadPreset` in widget config +- Server includes `upload_preset` in signed params +- Use Cloudinary Node.js SDK v2 on server + +For complete implementation patterns, client/server code examples, and troubleshooting, see [references/signed-uploads.md](references/signed-uploads.md) + +## Video Patterns + +- βœ… **Display a video** β†’ Use **AdvancedVideo** (`@cloudinary/react`). It just displays a video (with optional transformations). Not a full player. +- βœ… **A video player** β†’ Use **Cloudinary Video Player** (`cloudinary-video-player`). That is the actual player (styled UI, controls, playlists, etc.). + +### ⚠️ IMPORTANT: Two Different Approaches + +**1. AdvancedVideo** (`@cloudinary/react`) β€” For **displaying** a video +- React component similar to `AdvancedImage`; just displays a video with Cloudinary transformations +- Not a full "player" β€” it's video display (native HTML5 video with optional controls) +- Use when: user wants to show/display a video. Works with `cld.video()` like images with `cld.image()` + +**2. Cloudinary Video Player** (`cloudinary-video-player`) β€” The **player** +- Full-featured video player (styled UI, controls, playlists). Use when the user asks for a "video player." +- **Use imperative video element only** (create with document.createElement, append to container ref); do not pass a React-managed `