-
Notifications
You must be signed in to change notification settings - Fork 1
Add cloudinary-next skill #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
d847e9c
Add cloudinary-next skill
eportis-cloudinary 6e4affa
Add updated front matter to Next skill
eportis-cloudinary 7b8a2a3
Add next skill to repo Readme
jackieros 37cf776
Delete all instances of the generated `> Source: converted from the u…
jackieros 3eff58b
Removed implication that only signed uploads are for production plus …
jackieros File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| --- | ||
| name: cloudinary-next | ||
| description: Guidance for building, debugging, and reviewing next.js apps that use cloudinary through next-cloudinary and the cloudinary node sdk v2. Use for tasks involving CldImage, CldVideoPlayer, CldUploadWidget, CldUploadButton, CldOgImage, GetCldImageUrl, GetCldOgImageUrl, GetCldVideoUrl, signed uploads, server uploads, deletes, transformations, responsive images, overlays, social cards, environment variables, and common Cloudinary errors. | ||
| license: MIT | ||
| metadata: | ||
| author: cloudinary | ||
| version: '1.0.0' | ||
| --- | ||
|
|
||
| # Cloudinary Next | ||
|
|
||
| ## Purpose | ||
|
|
||
| Use this skill to help developers build, debug, or review Next.js projects that integrate Cloudinary through `next-cloudinary` and, for server-only operations, the Cloudinary Node SDK v2. | ||
|
|
||
| This skill is organized for progressive loading. Do not load every reference file by default. Start with the workflow below, then load only the reference files needed for the user's task. | ||
|
|
||
| ## Core workflow | ||
|
|
||
| 1. Classify the user's goal before writing code: | ||
| - render a transformed Cloudinary image in JSX | ||
| - generate a Cloudinary URL string | ||
| - embed a video player | ||
| - upload from the browser | ||
| - perform a signed upload | ||
| - upload from the server | ||
| - delete an asset | ||
| - build an overlay or text overlay | ||
| - generate an OG/social image | ||
| - fix TypeScript, environment, import, runtime, or upload errors | ||
| - review an existing implementation | ||
| 2. Load the relevant reference file from the map below. | ||
| 3. Apply the non-negotiable rules in this `SKILL.md` before using any detailed reference. | ||
| 4. For code review or debugging, load `references/troubleshooting.md` plus the task-specific reference. | ||
| 5. When a detail is version-sensitive or not covered here, consult the official documentation linked in `references/official-docs.md` and prefer official prop names and event names over memory. | ||
|
|
||
| ## Non-negotiable rules | ||
|
|
||
| - Use `next-cloudinary` for Next.js components and URL helpers: `CldImage`, `CldVideoPlayer`, `CldUploadWidget`, `CldUploadButton`, `CldOgImage`, `getCldImageUrl`, `getCldOgImageUrl`, and `getCldVideoUrl`. | ||
| - Use the Cloudinary Node SDK v2 only for server-side operations: `import { v2 as cloudinary } from 'cloudinary'`. | ||
| - Never expose `CLOUDINARY_API_SECRET` to the browser. Never create `NEXT_PUBLIC_CLOUDINARY_API_SECRET`. | ||
| - Put upload widgets, video player UI, and any component with React event handlers behind a Client Component boundary with `'use client'`. | ||
| - `CldImage` may be used from a Server Component for static rendering, but if you add client-only props such as `onLoad` or local state, move it into a Client Component. | ||
| - Do not import `cloudinary` in Client Components or Edge runtime code. Server Actions and route handlers that import `cloudinary` must run on the Node.js runtime. | ||
| - Use documented `next-cloudinary` prop names and shapes. Do not infer prop names from Cloudinary URL transformation parameters. | ||
| - Use `onSuccess` for upload widget success handling. Do not use deprecated upload callback names unless the installed version explicitly documents them. | ||
| - For deletes, pass a public ID, not a delivery URL. Pass `resource_type` when deleting videos or raw assets, and use `invalidate: true` when CDN cache invalidation is desired. | ||
|
|
||
| ## Reference map | ||
|
|
||
| Load the smallest useful set of references: | ||
|
|
||
| - `references/official-docs.md` — official documentation links and the global prop-name rule. | ||
| - `references/api-decision-tree.md` — choose the correct Cloudinary API/component/helper for a user goal. | ||
| - `references/project-setup.md` — install packages, configure Next.js image domains, configure upload presets, and create starter setup files. | ||
| - `references/environment.md` — environment variables and TypeScript process env typing. | ||
| - `references/imports.md` — correct import paths and server/client boundaries. | ||
| - `references/cldimage.md` — `CldImage`, sample assets, and `getCldImageUrl` usage. | ||
| - `references/cldimage-transformations.md` — transformation props, generative editing, optimization, raw transformations, and crop traps. | ||
| - `references/responsive-images.md` — responsive image sizing and `sizes` guidance. | ||
| - `references/video-player.md` — `CldVideoPlayer`, required CSS, and client-only player setup. | ||
| - `references/upload-widget.md` — browser uploads, upload widget/button usage, events, and signed-vs-unsigned tradeoffs. | ||
| - `references/signed-uploads.md` — App Router signature endpoint pattern. | ||
| - `references/server-upload-delete.md` — Server Action and route-handler upload/delete patterns with the Node SDK v2. | ||
| - `references/overlays.md` — image overlays and text overlay prop shapes. | ||
| - `references/og-images.md` — App Router and Pages Router OG/social card patterns. | ||
| - `references/typescript.md` — upload result narrowing, server upload result types, refs, and avoiding `any`. | ||
| - `references/troubleshooting.md` — common error messages and fixes. | ||
| - `references/quick-checklist.md` — final code-review checklist and best practices. | ||
|
|
||
| ## Output expectations | ||
|
|
||
| When generating code, include the file path, the complete relevant code block, and a short note about where the code runs: Client Component, Server Component, Server Action, or route handler. | ||
|
|
||
| When reviewing code, report issues in this order: secret exposure, server/client boundary mistakes, import/runtime mistakes, incorrect component/helper choice, incorrect prop/event names, missing TypeScript narrowing, and missing cache invalidation or resource type handling. | ||
|
|
||
| When using reusable templates, copy from `assets/app-router-signature-route.ts`, `assets/server-action-upload.ts`, or `assets/server-action-delete.ts` and adapt names, folders, and return values to the user's project. |
33 changes: 33 additions & 0 deletions
33
skills/cloudinary-next/assets/app-router-signature-route.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { v2 as cloudinary } from 'cloudinary'; | ||
|
|
||
| export const runtime = 'nodejs'; | ||
|
|
||
| type ParamsToSign = Record<string, string | number | boolean | undefined>; | ||
|
|
||
| function requiredEnv(name: string): string { | ||
| const value = process.env[name]; | ||
| if (!value) throw new Error(`Missing required environment variable: ${name}`); | ||
| return value; | ||
| } | ||
|
|
||
| cloudinary.config({ | ||
| cloud_name: requiredEnv('NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME'), | ||
| api_key: requiredEnv('NEXT_PUBLIC_CLOUDINARY_API_KEY'), | ||
| api_secret: requiredEnv('CLOUDINARY_API_SECRET'), | ||
| }); | ||
|
|
||
| export async function POST(request: Request) { | ||
| const body = (await request.json()) as { paramsToSign?: ParamsToSign }; | ||
|
|
||
| if (!body.paramsToSign || typeof body.paramsToSign !== 'object') { | ||
| return Response.json({ error: 'Missing paramsToSign' }, { status: 400 }); | ||
| } | ||
|
|
||
| const signature = cloudinary.utils.api_sign_request( | ||
| body.paramsToSign, | ||
| requiredEnv('CLOUDINARY_API_SECRET'), | ||
| ); | ||
|
|
||
| // The upload widget expects this exact field name. | ||
| return Response.json({ signature }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| 'use server'; | ||
|
|
||
| import { v2 as cloudinary } from 'cloudinary'; | ||
|
|
||
| type ResourceType = 'image' | 'video' | 'raw'; | ||
|
|
||
| function requiredEnv(name: string): string { | ||
| const value = process.env[name]; | ||
| if (!value) throw new Error(`Missing required environment variable: ${name}`); | ||
| return value; | ||
| } | ||
|
|
||
| cloudinary.config({ | ||
| cloud_name: requiredEnv('NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME'), | ||
| api_key: requiredEnv('NEXT_PUBLIC_CLOUDINARY_API_KEY'), | ||
| api_secret: requiredEnv('CLOUDINARY_API_SECRET'), | ||
| }); | ||
|
|
||
| export async function deleteAsset( | ||
| publicId: string, | ||
| resourceType: ResourceType = 'image', | ||
| ): Promise<{ result: string }> { | ||
| if (!publicId) throw new Error('Missing publicId'); | ||
|
|
||
| const result = await cloudinary.uploader.destroy(publicId, { | ||
| resource_type: resourceType, | ||
| invalidate: true, | ||
| }); | ||
|
|
||
| return { result: result.result }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| 'use server'; | ||
|
|
||
| import { v2 as cloudinary, type UploadApiResponse } from 'cloudinary'; | ||
|
|
||
| function requiredEnv(name: string): string { | ||
| const value = process.env[name]; | ||
| if (!value) throw new Error(`Missing required environment variable: ${name}`); | ||
| return value; | ||
| } | ||
|
|
||
| cloudinary.config({ | ||
| cloud_name: requiredEnv('NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME'), | ||
| api_key: requiredEnv('NEXT_PUBLIC_CLOUDINARY_API_KEY'), | ||
| api_secret: requiredEnv('CLOUDINARY_API_SECRET'), | ||
| }); | ||
|
|
||
| export async function uploadImage(formData: FormData): Promise<{ publicId: string; url: string }> { | ||
| const file = formData.get('file'); | ||
| if (!(file instanceof File)) throw new Error('No file provided'); | ||
|
|
||
| const arrayBuffer = await file.arrayBuffer(); | ||
| const buffer = Buffer.from(arrayBuffer); | ||
|
|
||
| const result = await new Promise<UploadApiResponse>((resolve, reject) => { | ||
| cloudinary.uploader | ||
| .upload_stream({ folder: 'user-uploads', resource_type: 'auto' }, (error, response) => { | ||
| if (error) return reject(error); | ||
| if (!response) return reject(new Error('Cloudinary upload returned no response')); | ||
| resolve(response); | ||
| }) | ||
| .end(buffer); | ||
| }); | ||
|
|
||
| return { publicId: result.public_id, url: result.secure_url }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # API decision tree | ||
|
|
||
| Use this first when the user is unsure which Cloudinary component, helper, or SDK API to use. | ||
|
|
||
|
|
||
| ## Which Cloudinary API do I use? (decision tree) | ||
|
|
||
| This is the single biggest source of confusion in Next.js + Cloudinary. **Pick the right tool, then the rest is mechanical.** | ||
|
|
||
| | You want to… | Use this | Where it runs | | ||
| | --- | --- | --- | | ||
| | Render an `<img>` of a Cloudinary asset in JSX | **`CldImage`** (`next-cloudinary`) | Client component (`"use client"`) **or** Server Component | | ||
| | Get a Cloudinary image URL as a string (for `<meta>`, `<link>`, an API response, an OG image, etc.) | **`getCldImageUrl`** / **`getCldOgImageUrl`** (`next-cloudinary`) | Anywhere — server, client, route handlers, `generateMetadata` | | ||
| | Embed a video player UI | **`CldVideoPlayer`** (`next-cloudinary`) | Client component (`"use client"`) — required | | ||
| | Let the user upload from the browser | **`CldUploadWidget`** or **`CldUploadButton`** (`next-cloudinary`) | Client component (`"use client"`) — required | | ||
| | Generate an OG/social image | App Router → **`getCldOgImageUrl`** in `generateMetadata`. Pages Router → **`CldOgImage`** in the page | Server (App) / Pages Head | | ||
| | Upload a file **from your server** (e.g. a Server Action receiving a `FormData`) | **`cloudinary.uploader.upload`** / **`upload_stream`** (Node SDK v2, `cloudinary`) | Server only (`"use server"` action or route handler) | | ||
| | Delete an asset | **`cloudinary.uploader.destroy`** (Node SDK v2) | Server only | | ||
| | Sign upload widget params on the server | **`cloudinary.utils.api_sign_request`** (Node SDK v2) | Route handler (`/api/sign-cloudinary-params`) | | ||
| | List / search assets, manage tags, etc. | **Cloudinary Admin API** via Node SDK v2 (`cloudinary.api.*`) | Server only | | ||
|
|
||
| **Common confusion to flush out before writing code:** | ||
| - ❌ "Should I use `cloudinary.uploader.upload` or `cloudinary.image` or `cloudinary.url`?" → They do **different** things. `cloudinary.uploader.upload` (Node SDK, server) **uploads** a file. `cloudinary.url()` / `cloudinary.image()` (Node SDK, server) build a **URL/HTML tag** from a public ID — for that job, prefer **`getCldImageUrl`** or **`<CldImage>`** in Next.js. They are not interchangeable. | ||
| - ❌ Do **not** call `cloudinary.uploader.upload` from a Client Component or browser code — it requires `api_secret`, which must never reach the browser. | ||
| - ❌ Do **not** use `<CldUploadWidget>` or `<CldVideoPlayer>` in a Server Component that has not been wrapped in `"use client"` — they use React state/effects and will throw. `CldImage` can be used from a Server Component for static rendering, but client event handlers require a Client Component. | ||
| - ✅ For "show an image with transformations" the answer is almost always **`<CldImage>`**. For "I just need the URL string" the answer is **`getCldImageUrl`**. |
87 changes: 87 additions & 0 deletions
87
skills/cloudinary-next/references/cldimage-transformations.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # CldImage transformations | ||
|
|
||
| Use this when applying image transformations. Prefer documented `next-cloudinary` prop names over raw URL transformation parameter names. | ||
|
|
||
| ## CldImage — transformations (the canonical prop reference) | ||
|
|
||
| Use **only** the prop names and shapes below (and on https://next.cloudinary.dev/cldimage/configuration). These are not the same as the underlying URL transformation params. | ||
|
|
||
| ### Basic transformations | ||
| | Prop | Type | Example | | ||
| | --- | --- | --- | | ||
| | `crop` | `string \| object \| object[]` | `"fill"`, `{ type: 'thumb', source: true }` | | ||
| | `gravity` | `string` | `"face"`, `"auto"` | | ||
| | `aspectRatio` | `string` | `"16:9"` (requires `fill` + a crop mode that crops; omit `width`/`height`) | | ||
| | `angle` | `number \| string` | `45` | | ||
| | `background` | `string` | `"blue"`, `"rgb:0000ff"` | | ||
|
|
||
| ### Generative AI / advanced editing | ||
| | Prop | Type | Example | | ||
| | --- | --- | --- | | ||
| | `removeBackground` | `boolean \| string` | `removeBackground` | | ||
| | `replaceBackground` | `boolean \| string \| object` | `"fish tank"` or `{ prompt: 'fish tank', seed: 3 }` | | ||
| | `fillBackground` | `boolean \| object` | `fillBackground` or `{ crop: 'pad', gravity: 'south', prompt: 'cupcakes' }` | | ||
| | `extract` | `string \| string[] \| object` | `"space jellyfish"` | | ||
| | `remove` | `string \| string[] \| object` | `"apple"` or `{ prompt: 'apple', multiple: true, removeShadow: true }` | | ||
| | `replace` | `string[] \| object` | `['apple', 'banana']` or `{ from: 'apple', to: 'banana' }` | | ||
| | `recolor` | `[string, string] \| object` | `['duck', 'blue']` | | ||
| | `restore` | `boolean` | `restore` | | ||
| | `enhance` | `boolean` | `enhance` | | ||
|
|
||
| ### Optimization & delivery | ||
| | Prop | Type | Default | Notes | | ||
| | --- | --- | --- | --- | | ||
| | `format` | `string` | `auto` | Override only for specific cases (e.g. `"png"`) | | ||
| | `quality` | `string \| number` | `auto` | Use `"default"` to omit `q_*` (needed for `flags: ['keep_iptc']`) | | ||
| | `dpr` | `number \| string` | — | `"2.0"` or `"auto"` | | ||
| | `unoptimized` | `boolean` | — | Skips both `f_auto` and `q_auto` | | ||
| | `deliveryType` | `string` | `upload` | e.g. `"fetch"` | | ||
| | `assetType` | `string` | `image` | Use `"video"` to make a thumbnail from a video public ID | | ||
| | `flags` | `string[]` | — | e.g. `['keep_iptc']` | | ||
| | `version` | `number \| string` | — | e.g. `1234` | | ||
|
|
||
| ### Escape hatches | ||
| | Prop | Use when… | | ||
| | --- | --- | | ||
| | `effects` | You want to chain multiple effect blocks: `effects={[{ background: 'green' }, { gradientFade: true }]}` | | ||
| | `namedTransformations` | You have a named transformation in your Cloudinary account: `namedTransformations={['my-named']}` | | ||
| | `rawTransformations` | You need a transformation not exposed as a prop: `rawTransformations={['e_blur:2000']}` | | ||
| | `preserveTransformations` | The `src` is a full Cloudinary URL with transformations to keep | | ||
| | `strictTransformations` | Account requires strict mode (only `namedTransformations` allowed) | | ||
|
|
||
| ❌ **Do NOT**: pass `f_auto`, `q_auto`, `c_fill`, etc. via `rawTransformations` and also via the named props — pick one. Mixing causes duplicates and unpredictable URLs. | ||
|
|
||
| ### Cropping — the `fill` prop trap | ||
|
|
||
| Two distinct things share the word "fill": | ||
| - **`fill={true}`** (Next Image prop) — render the image absolutely-positioned, filling the parent. No `width`/`height` required; parent must be `position: relative`. Combine with `aspectRatio` for ratio-locked containers. | ||
| - **`crop="fill"`** (Cloudinary crop mode) — Cloudinary **crops** the asset to the requested width/height. | ||
|
|
||
| These are independent. You can use both: `<CldImage fill crop="fill" aspectRatio="16:9" sizes="100vw" alt="..." />`. | ||
|
|
||
| ❌ **WRONG**: Using `aspectRatio` with explicit `width`/`height` and no `fill` — `aspectRatio` won't apply because Next Image needs concrete dimensions. | ||
| ✅ **CORRECT**: For `aspectRatio`, also pass `fill` and a crop mode (e.g. `"fill"`, `"thumb"`); omit `width`/`height`. | ||
|
|
||
| ### Two-stage cropping for dynamic crop modes | ||
|
|
||
| Dynamic crop modes (`thumb`, `crop` with no coordinates, etc.) produce **different visual results at different sizes**, which interacts badly with responsive `srcset`. To keep results consistent across breakpoints, crop the **source** image first, then let responsive sizing resize it: | ||
|
|
||
| ```tsx | ||
| <CldImage | ||
| src="<Public ID>" | ||
| width={960} | ||
| height={960} | ||
| crop={{ type: 'thumb', source: true }} // source: true → applied to original asset | ||
| sizes="100vw" | ||
| alt="" | ||
| /> | ||
| ``` | ||
|
|
||
| You can also pin the source crop dimensions: | ||
|
|
||
| ```tsx | ||
| crop={{ type: 'thumb', width: 1200, height: 1200, source: true }} | ||
| ``` | ||
|
|
||
| ❌ **WRONG**: Using `crop="thumb"` with `sizes="100vw"` — the thumbnail content shifts as the viewport changes. | ||
| ✅ **CORRECT**: Use object form with `source: true` for `thumb`-style crops. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eportis-cloudinary - Added to the repo readme. Pls check.