From 8eab812909869119b9adacebe0e9ec06f75d0bd5 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 13:54:11 +0100 Subject: [PATCH 1/6] Move skills into category directories Co-Authored-By: Claude Fable 5.1 --- skills/{ => frameworks}/cloudinary-next/SKILL.md | 0 .../cloudinary-next/assets/app-router-signature-route.ts | 0 .../cloudinary-next/assets/server-action-delete.ts | 0 .../cloudinary-next/assets/server-action-upload.ts | 0 .../cloudinary-next/references/api-decision-tree.md | 0 .../cloudinary-next/references/cldimage-transformations.md | 0 skills/{ => frameworks}/cloudinary-next/references/cldimage.md | 0 skills/{ => frameworks}/cloudinary-next/references/environment.md | 0 skills/{ => frameworks}/cloudinary-next/references/imports.md | 0 .../{ => frameworks}/cloudinary-next/references/official-docs.md | 0 skills/{ => frameworks}/cloudinary-next/references/og-images.md | 0 skills/{ => frameworks}/cloudinary-next/references/overlays.md | 0 .../{ => frameworks}/cloudinary-next/references/project-setup.md | 0 .../cloudinary-next/references/quick-checklist.md | 0 .../cloudinary-next/references/responsive-images.md | 0 .../cloudinary-next/references/server-upload-delete.md | 0 .../{ => frameworks}/cloudinary-next/references/signed-uploads.md | 0 .../cloudinary-next/references/troubleshooting.md | 0 skills/{ => frameworks}/cloudinary-next/references/typescript.md | 0 .../{ => frameworks}/cloudinary-next/references/upload-widget.md | 0 .../{ => frameworks}/cloudinary-next/references/video-player.md | 0 skills/{ => frameworks}/cloudinary-react/SKILL.md | 0 .../cloudinary-react/references/signed-uploads.md | 0 .../cloudinary-react/references/troubleshooting.md | 0 .../cloudinary-react/references/typescript-patterns.md | 0 .../{ => frameworks}/cloudinary-react/references/video-player.md | 0 skills/{ => platform}/cloudinary-docs/SKILL.md | 0 skills/{ => platform}/cloudinary-transformations/SKILL.md | 0 .../cloudinary-transformations/references/advanced-features.md | 0 .../cloudinary-transformations/references/ai-transformations.md | 0 .../cloudinary-transformations/references/debugging.md | 0 .../cloudinary-transformations/references/examples.md | 0 .../references/named-transformations.md | 0 .../cloudinary-transformations/references/responsive-images.md | 0 .../cloudinary-transformations/references/transformation-costs.md | 0 .../references/video-transformations.md | 0 36 files changed, 0 insertions(+), 0 deletions(-) rename skills/{ => frameworks}/cloudinary-next/SKILL.md (100%) rename skills/{ => frameworks}/cloudinary-next/assets/app-router-signature-route.ts (100%) rename skills/{ => frameworks}/cloudinary-next/assets/server-action-delete.ts (100%) rename skills/{ => frameworks}/cloudinary-next/assets/server-action-upload.ts (100%) rename skills/{ => frameworks}/cloudinary-next/references/api-decision-tree.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/cldimage-transformations.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/cldimage.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/environment.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/imports.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/official-docs.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/og-images.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/overlays.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/project-setup.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/quick-checklist.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/responsive-images.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/server-upload-delete.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/signed-uploads.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/troubleshooting.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/typescript.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/upload-widget.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/video-player.md (100%) rename skills/{ => frameworks}/cloudinary-react/SKILL.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/signed-uploads.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/troubleshooting.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/typescript-patterns.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/video-player.md (100%) rename skills/{ => platform}/cloudinary-docs/SKILL.md (100%) rename skills/{ => platform}/cloudinary-transformations/SKILL.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/advanced-features.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/ai-transformations.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/debugging.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/examples.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/named-transformations.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/responsive-images.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/transformation-costs.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/video-transformations.md (100%) diff --git a/skills/cloudinary-next/SKILL.md b/skills/frameworks/cloudinary-next/SKILL.md similarity index 100% rename from skills/cloudinary-next/SKILL.md rename to skills/frameworks/cloudinary-next/SKILL.md diff --git a/skills/cloudinary-next/assets/app-router-signature-route.ts b/skills/frameworks/cloudinary-next/assets/app-router-signature-route.ts similarity index 100% rename from skills/cloudinary-next/assets/app-router-signature-route.ts rename to skills/frameworks/cloudinary-next/assets/app-router-signature-route.ts diff --git a/skills/cloudinary-next/assets/server-action-delete.ts b/skills/frameworks/cloudinary-next/assets/server-action-delete.ts similarity index 100% rename from skills/cloudinary-next/assets/server-action-delete.ts rename to skills/frameworks/cloudinary-next/assets/server-action-delete.ts diff --git a/skills/cloudinary-next/assets/server-action-upload.ts b/skills/frameworks/cloudinary-next/assets/server-action-upload.ts similarity index 100% rename from skills/cloudinary-next/assets/server-action-upload.ts rename to skills/frameworks/cloudinary-next/assets/server-action-upload.ts diff --git a/skills/cloudinary-next/references/api-decision-tree.md b/skills/frameworks/cloudinary-next/references/api-decision-tree.md similarity index 100% rename from skills/cloudinary-next/references/api-decision-tree.md rename to skills/frameworks/cloudinary-next/references/api-decision-tree.md diff --git a/skills/cloudinary-next/references/cldimage-transformations.md b/skills/frameworks/cloudinary-next/references/cldimage-transformations.md similarity index 100% rename from skills/cloudinary-next/references/cldimage-transformations.md rename to skills/frameworks/cloudinary-next/references/cldimage-transformations.md diff --git a/skills/cloudinary-next/references/cldimage.md b/skills/frameworks/cloudinary-next/references/cldimage.md similarity index 100% rename from skills/cloudinary-next/references/cldimage.md rename to skills/frameworks/cloudinary-next/references/cldimage.md diff --git a/skills/cloudinary-next/references/environment.md b/skills/frameworks/cloudinary-next/references/environment.md similarity index 100% rename from skills/cloudinary-next/references/environment.md rename to skills/frameworks/cloudinary-next/references/environment.md diff --git a/skills/cloudinary-next/references/imports.md b/skills/frameworks/cloudinary-next/references/imports.md similarity index 100% rename from skills/cloudinary-next/references/imports.md rename to skills/frameworks/cloudinary-next/references/imports.md diff --git a/skills/cloudinary-next/references/official-docs.md b/skills/frameworks/cloudinary-next/references/official-docs.md similarity index 100% rename from skills/cloudinary-next/references/official-docs.md rename to skills/frameworks/cloudinary-next/references/official-docs.md diff --git a/skills/cloudinary-next/references/og-images.md b/skills/frameworks/cloudinary-next/references/og-images.md similarity index 100% rename from skills/cloudinary-next/references/og-images.md rename to skills/frameworks/cloudinary-next/references/og-images.md diff --git a/skills/cloudinary-next/references/overlays.md b/skills/frameworks/cloudinary-next/references/overlays.md similarity index 100% rename from skills/cloudinary-next/references/overlays.md rename to skills/frameworks/cloudinary-next/references/overlays.md diff --git a/skills/cloudinary-next/references/project-setup.md b/skills/frameworks/cloudinary-next/references/project-setup.md similarity index 100% rename from skills/cloudinary-next/references/project-setup.md rename to skills/frameworks/cloudinary-next/references/project-setup.md diff --git a/skills/cloudinary-next/references/quick-checklist.md b/skills/frameworks/cloudinary-next/references/quick-checklist.md similarity index 100% rename from skills/cloudinary-next/references/quick-checklist.md rename to skills/frameworks/cloudinary-next/references/quick-checklist.md diff --git a/skills/cloudinary-next/references/responsive-images.md b/skills/frameworks/cloudinary-next/references/responsive-images.md similarity index 100% rename from skills/cloudinary-next/references/responsive-images.md rename to skills/frameworks/cloudinary-next/references/responsive-images.md diff --git a/skills/cloudinary-next/references/server-upload-delete.md b/skills/frameworks/cloudinary-next/references/server-upload-delete.md similarity index 100% rename from skills/cloudinary-next/references/server-upload-delete.md rename to skills/frameworks/cloudinary-next/references/server-upload-delete.md diff --git a/skills/cloudinary-next/references/signed-uploads.md b/skills/frameworks/cloudinary-next/references/signed-uploads.md similarity index 100% rename from skills/cloudinary-next/references/signed-uploads.md rename to skills/frameworks/cloudinary-next/references/signed-uploads.md diff --git a/skills/cloudinary-next/references/troubleshooting.md b/skills/frameworks/cloudinary-next/references/troubleshooting.md similarity index 100% rename from skills/cloudinary-next/references/troubleshooting.md rename to skills/frameworks/cloudinary-next/references/troubleshooting.md diff --git a/skills/cloudinary-next/references/typescript.md b/skills/frameworks/cloudinary-next/references/typescript.md similarity index 100% rename from skills/cloudinary-next/references/typescript.md rename to skills/frameworks/cloudinary-next/references/typescript.md diff --git a/skills/cloudinary-next/references/upload-widget.md b/skills/frameworks/cloudinary-next/references/upload-widget.md similarity index 100% rename from skills/cloudinary-next/references/upload-widget.md rename to skills/frameworks/cloudinary-next/references/upload-widget.md diff --git a/skills/cloudinary-next/references/video-player.md b/skills/frameworks/cloudinary-next/references/video-player.md similarity index 100% rename from skills/cloudinary-next/references/video-player.md rename to skills/frameworks/cloudinary-next/references/video-player.md diff --git a/skills/cloudinary-react/SKILL.md b/skills/frameworks/cloudinary-react/SKILL.md similarity index 100% rename from skills/cloudinary-react/SKILL.md rename to skills/frameworks/cloudinary-react/SKILL.md diff --git a/skills/cloudinary-react/references/signed-uploads.md b/skills/frameworks/cloudinary-react/references/signed-uploads.md similarity index 100% rename from skills/cloudinary-react/references/signed-uploads.md rename to skills/frameworks/cloudinary-react/references/signed-uploads.md diff --git a/skills/cloudinary-react/references/troubleshooting.md b/skills/frameworks/cloudinary-react/references/troubleshooting.md similarity index 100% rename from skills/cloudinary-react/references/troubleshooting.md rename to skills/frameworks/cloudinary-react/references/troubleshooting.md diff --git a/skills/cloudinary-react/references/typescript-patterns.md b/skills/frameworks/cloudinary-react/references/typescript-patterns.md similarity index 100% rename from skills/cloudinary-react/references/typescript-patterns.md rename to skills/frameworks/cloudinary-react/references/typescript-patterns.md diff --git a/skills/cloudinary-react/references/video-player.md b/skills/frameworks/cloudinary-react/references/video-player.md similarity index 100% rename from skills/cloudinary-react/references/video-player.md rename to skills/frameworks/cloudinary-react/references/video-player.md diff --git a/skills/cloudinary-docs/SKILL.md b/skills/platform/cloudinary-docs/SKILL.md similarity index 100% rename from skills/cloudinary-docs/SKILL.md rename to skills/platform/cloudinary-docs/SKILL.md diff --git a/skills/cloudinary-transformations/SKILL.md b/skills/platform/cloudinary-transformations/SKILL.md similarity index 100% rename from skills/cloudinary-transformations/SKILL.md rename to skills/platform/cloudinary-transformations/SKILL.md diff --git a/skills/cloudinary-transformations/references/advanced-features.md b/skills/platform/cloudinary-transformations/references/advanced-features.md similarity index 100% rename from skills/cloudinary-transformations/references/advanced-features.md rename to skills/platform/cloudinary-transformations/references/advanced-features.md diff --git a/skills/cloudinary-transformations/references/ai-transformations.md b/skills/platform/cloudinary-transformations/references/ai-transformations.md similarity index 100% rename from skills/cloudinary-transformations/references/ai-transformations.md rename to skills/platform/cloudinary-transformations/references/ai-transformations.md diff --git a/skills/cloudinary-transformations/references/debugging.md b/skills/platform/cloudinary-transformations/references/debugging.md similarity index 100% rename from skills/cloudinary-transformations/references/debugging.md rename to skills/platform/cloudinary-transformations/references/debugging.md diff --git a/skills/cloudinary-transformations/references/examples.md b/skills/platform/cloudinary-transformations/references/examples.md similarity index 100% rename from skills/cloudinary-transformations/references/examples.md rename to skills/platform/cloudinary-transformations/references/examples.md diff --git a/skills/cloudinary-transformations/references/named-transformations.md b/skills/platform/cloudinary-transformations/references/named-transformations.md similarity index 100% rename from skills/cloudinary-transformations/references/named-transformations.md rename to skills/platform/cloudinary-transformations/references/named-transformations.md diff --git a/skills/cloudinary-transformations/references/responsive-images.md b/skills/platform/cloudinary-transformations/references/responsive-images.md similarity index 100% rename from skills/cloudinary-transformations/references/responsive-images.md rename to skills/platform/cloudinary-transformations/references/responsive-images.md diff --git a/skills/cloudinary-transformations/references/transformation-costs.md b/skills/platform/cloudinary-transformations/references/transformation-costs.md similarity index 100% rename from skills/cloudinary-transformations/references/transformation-costs.md rename to skills/platform/cloudinary-transformations/references/transformation-costs.md diff --git a/skills/cloudinary-transformations/references/video-transformations.md b/skills/platform/cloudinary-transformations/references/video-transformations.md similarity index 100% rename from skills/cloudinary-transformations/references/video-transformations.md rename to skills/platform/cloudinary-transformations/references/video-transformations.md From 62490331819e8a5bbedcd25253adfd71425b8a99 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 13:55:04 +0100 Subject: [PATCH 2/6] Add skills discovery check Runs the real skills CLI against the checkout and fails when the number of discovered skills differs from the number of SKILL.md files, so a skill dropped by bad frontmatter or wrong depth cannot merge silently. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/skills-discovery.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/skills-discovery.yml diff --git a/.github/workflows/skills-discovery.yml b/.github/workflows/skills-discovery.yml new file mode 100644 index 0000000..12fd496 --- /dev/null +++ b/.github/workflows/skills-discovery.yml @@ -0,0 +1,26 @@ +name: Skills discovery +on: + pull_request: + push: + branches: [main] +jobs: + discovery: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Every SKILL.md is discovered by the skills CLI + env: + INSTALL_INTERNAL_SKILLS: "1" + run: | + expected=$(find skills -name SKILL.md | wc -l | tr -d ' ') + output=$(npx -y skills@latest add . --list 2>&1 || true) + found=$(printf '%s' "$output" | grep -oE 'Found [0-9]+ skills?' | grep -oE '[0-9]+' || echo 0) + echo "expected=$expected found=$found" + if [ "$found" != "$expected" ]; then + echo "::error::Discovery mismatch. A SKILL.md is being dropped (bad frontmatter, wrong depth, or missing name/description)." + printf '%s\n' "$output" + exit 1 + fi From bbb8972f6bd9f9e8ed57f3f853b5a1ec039fd029 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 13:55:40 +0100 Subject: [PATCH 3/6] Document catalog layout and authoring conventions README groups the skills table by category, leads the install section with --skill examples. CONTRIBUTING.md records the layout, frontmatter, description-shape, cross-reference, versioning and local-check rules. specs/ is gitignored. Co-Authored-By: Claude Fable 5.1 --- .gitignore | 1 + CONTRIBUTING.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 20 +++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6de900 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +specs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cb69f05 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contributing + +Rules for adding or changing skills in this repo. The `skills` CLI is unforgiving +about layout and frontmatter: a skill that breaks these rules is dropped from +discovery with no warning, so read this before opening a PR. + +## Layout + +Every skill lives at `skills///SKILL.md`, exactly two directories +deep. The CLI does not look any deeper, so a skill nested at depth three is +invisible to users. + +Categories: + +- `platform` — reference skills for a Cloudinary capability (uploads, + transformations, docs lookup) +- `frameworks` — patterns for using Cloudinary in a specific framework or SDK +- `use-cases` — end-to-end recipes that combine several capabilities +- `utilities` — tools that do a job using Cloudinary + +Category directories are created with their first skill. Do not add a README or +placeholder to an empty category. + +Users install skills by name, not by path, so the category is invisible to them. +Moving a skill between categories does not affect existing installs. + +## Frontmatter + +```yaml +--- +name: cloudinary-example +description: "Reference for . Use when ." +license: MIT +metadata: + author: cloudinary + version: '1.0.0' +--- +``` + +- `name` must equal the directory name. +- `name` and `description` must both be strings. If either is missing, or the + frontmatter fails to parse, the skill silently vanishes from discovery. +- A description containing a colon must be quoted. Unquoted, YAML reads the colon + as a nested key, the description is no longer a string, and the skill vanishes. +- Include `license`, `metadata.author`, and `metadata.version` (semver). +- Set `metadata.internal: true` on a work-in-progress skill that should not appear + in the default install. Users can reveal it with `INSTALL_INTERNAL_SKILLS=1`. + +## Description shapes + +Agents see the catalog as a flat list of names and descriptions, so every +description does two jobs. It opens with a clause that self-locates the skill in +that list, then follows with a "Use when..." sentence that carries the trigger +phrases. + +| Category | Opening shape | +|---|---| +| Platform | `Reference for .` | +| Frameworks | `Patterns for using Cloudinary in .` | +| Use-case | `End-to-end recipe for .` | +| Utility | `Tool that using Cloudinary.` | + +Rules: + +- The opening clause never contains a colon before its first period unless the + whole description is quoted. +- Trigger phrases live in the "Use when" sentence, not in the opening clause. + +Existing skills predate these shapes and are not yet retrofitted. New skills must +follow them. + +## Referencing other skills + +Refer to another skill by its name only, for example "use cloudinary-docs for +anything outside this skill's scope". Never reference another skill by file path. +Installed paths differ per agent and per install scope, so a path that works on +your machine will be wrong on someone else's. + +## Versioning + +Bump `metadata.version` on any content change to a skill, including its +references and assets. Moving or renaming files with identical content does not +bump the version; the lock-file hash is unchanged and `skills update` correctly +reports nothing to do. + +## Local check + +Before opening a PR, run the same check CI runs: + +```bash +INSTALL_INTERNAL_SKILLS=1 npx -y skills@latest add . --list +``` + +The `Found N skills` line must match the number of `SKILL.md` files under +`skills/`, and every skill name must appear in the list. A mismatch means a skill +is being dropped: check for bad frontmatter, an unquoted colon in the +description, a missing `name` or `description`, or a directory nested at the wrong +depth. CI (`.github/workflows/skills-discovery.yml`) fails the PR on the same +mismatch. diff --git a/README.md b/README.md index c9fe919..04d8bed 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,38 @@ The agent skills in this package help your AI coding assistant write correct Clo ## Available Skills +Skills are grouped by category below. The category is a folder in this repo only: you install a skill by its name, and the folder layout has no effect on how it is installed or used. + +### Platform + | Skill | Description | |---|---| | `cloudinary-docs` | Selects the most relevant markdown pages from the current documentation using the latest llms.txt. Use when answering Cloudinary questions or integrating Cloudinary into code. | | `cloudinary-transformations` | Turns natural language image and video transformation requirements into valid URL transformation strings that follow Cloudinary best practices. Use when building delivery URLs, applying transformations, optimizing media, or debugging transformation syntax errors. | + +### Frameworks + +| Skill | Description | +|---|---| | `cloudinary-react` | Provides opinionated React SDK patterns for configuration, common integration scenarios, and troubleshooting for frequent errors and TypeScript pitfalls. Use when developing React apps with Cloudinary. | | `cloudinary-next` | Provides opinionated Next.js SDK patterns for Server and Client Component boundaries, server-side uploads and deletes, and troubleshooting for frequent errors and TypeScript pitfalls. Use when developing Next.js apps with Cloudinary. | ## Install +Install the skills you need by name: + +```bash +npx skills add cloudinary-devs/skills --skill cloudinary-docs,cloudinary-transformations +npx skills add cloudinary-devs/skills --skill cloudinary-next +``` + +Or install everything: + ```bash npx skills add cloudinary-devs/skills ``` -Install all skills or select the ones you need. You can install globally or per project. +You can install globally or per project. > **💡 Tip:** [Cloudinary MCP servers](https://cloudinary.com/documentation/cloudinary_llm_mcp#mcp_servers) complement the Cloudinary Skills. Used together, skills and MCP servers improve the end-to-end workflow. For example, our skills spell out best practices such as using named transformations to apply the same transformation across many assets. Based on that, the model can decide which MCP tools to run to create named transformations that match your needs. From 4ba491fa054c031ae4116f7d5d6fff0f0d6f087a Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 14:39:27 +0100 Subject: [PATCH 4/6] Disable colour in discovery check output GitHub Actions sets CI, which makes the skills CLI colour its output, so 'Found 5 skills' arrived as 'Found \e[32m5\e[39m skills' and the grep never matched (found=0, expected=5). Set NO_COLOR and strip any remaining ANSI escapes before grepping. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/skills-discovery.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/skills-discovery.yml b/.github/workflows/skills-discovery.yml index 12fd496..d87bace 100644 --- a/.github/workflows/skills-discovery.yml +++ b/.github/workflows/skills-discovery.yml @@ -14,10 +14,13 @@ jobs: - name: Every SKILL.md is discovered by the skills CLI env: INSTALL_INTERNAL_SKILLS: "1" + NO_COLOR: "1" run: | expected=$(find skills -name SKILL.md | wc -l | tr -d ' ') output=$(npx -y skills@latest add . --list 2>&1 || true) - found=$(printf '%s' "$output" | grep -oE 'Found [0-9]+ skills?' | grep -oE '[0-9]+' || echo 0) + # Strip ANSI escapes: the CLI colours its output whenever CI is set. + plain=$(printf '%s' "$output" | sed -E 's/\x1b\[[0-9;?]*[a-zA-Z]//g') + found=$(printf '%s' "$plain" | grep -oE 'Found [0-9]+ skills?' | grep -oE '[0-9]+' || echo 0) echo "expected=$expected found=$found" if [ "$found" != "$expected" ]; then echo "::error::Discovery mismatch. A SKILL.md is being dropped (bad frontmatter, wrong depth, or missing name/description)." From 809ce3755707081356077e40a4e44d68eb531f07 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 13:54:39 +0100 Subject: [PATCH 5/6] Add cloudinary-upload skill Copied from feat/cloudinary-upload-skill into skills/platform/. Adds a Related Skills section pointing at cloudinary-next, cloudinary-react and cloudinary-docs, and appends the framework pointer to the description. Ships at 1.0.0. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 + skills/platform/cloudinary-upload/SKILL.md | 334 ++++++++++++++++++ .../references/chunked-uploads.md | 97 +++++ .../references/signed-uploads.md | 79 +++++ .../references/troubleshooting.md | 70 ++++ 5 files changed, 582 insertions(+) create mode 100644 skills/platform/cloudinary-upload/SKILL.md create mode 100644 skills/platform/cloudinary-upload/references/chunked-uploads.md create mode 100644 skills/platform/cloudinary-upload/references/signed-uploads.md create mode 100644 skills/platform/cloudinary-upload/references/troubleshooting.md diff --git a/README.md b/README.md index 04d8bed..f99998f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Skills are grouped by category below. The category is a folder in this repo only |---|---| | `cloudinary-docs` | Selects the most relevant markdown pages from the current documentation using the latest llms.txt. Use when answering Cloudinary questions or integrating Cloudinary into code. | | `cloudinary-transformations` | Turns natural language image and video transformation requirements into valid URL transformation strings that follow Cloudinary best practices. Use when building delivery URLs, applying transformations, optimizing media, or debugging transformation syntax errors. | +| `cloudinary-upload` | Guide uploading assets to Cloudinary — signed and unsigned uploads, upload presets, large files, remote/fetch uploads, and signature generation. Use when uploading files or URLs to Cloudinary, configuring upload presets, generating upload signatures, or debugging upload failures. For Next.js or React projects, use cloudinary-next or cloudinary-react alongside this skill. | ### Frameworks @@ -26,6 +27,7 @@ Install the skills you need by name: ```bash npx skills add cloudinary-devs/skills --skill cloudinary-docs,cloudinary-transformations +npx skills add cloudinary-devs/skills --skill cloudinary-upload npx skills add cloudinary-devs/skills --skill cloudinary-next ``` diff --git a/skills/platform/cloudinary-upload/SKILL.md b/skills/platform/cloudinary-upload/SKILL.md new file mode 100644 index 0000000..4b1b022 --- /dev/null +++ b/skills/platform/cloudinary-upload/SKILL.md @@ -0,0 +1,334 @@ +--- +name: cloudinary-upload +description: Guide uploading assets to Cloudinary — signed and unsigned uploads, upload presets, large files, remote/fetch uploads, and signature generation. Use when uploading files or URLs to Cloudinary, configuring upload presets, generating upload signatures, or debugging upload failures. For Next.js or React projects, use cloudinary-next or cloudinary-react alongside this skill. +license: MIT +metadata: + author: cloudinary + version: '1.0.0' +--- + +# Cloudinary Upload + +## When to Use + +- Uploading images, videos, or raw files to Cloudinary from any SDK or the REST API +- Generating upload signatures for signed uploads +- Configuring upload presets (signed or unsigned) +- Debugging upload failures (error responses, silent failures, unexpected behavior) +- Setting up remote/fetch uploads from external URLs +- Uploading large files (> 100 MB) + +## Quick Start + +**Unsigned upload (client-side, browser):** +``` +POST https://api.cloudinary.com/v1_1///upload + upload_preset: + file: +``` + +**Signed upload (server-side only):** +``` +POST https://api.cloudinary.com/v1_1///upload + api_key: + timestamp: + signature: + file: +``` + +**Remote/fetch upload (signed, server-side):** +``` +POST https://api.cloudinary.com/v1_1///upload + api_key: + timestamp: + signature: + file: + type: fetch +``` + +> **Note:** Replace `` with `image`, `video`, or `raw` — never omit it or rely on a default. See the `resource_type` section below. + +## Signed vs Unsigned + +| | Unsigned | Signed | +|---|---|---| +| **Who runs it** | Client (browser, mobile) | Server only | +| **Auth** | Upload preset name | API key + secret signature | +| **Upload preset** | Required | Optional (recommended for defaults) | +| **Parameter control** | Restricted whitelist only | Full | +| **Overwrite** | Always forced to `false` | Configurable | +| **Security** | Preset name visible in requests | API secret never leaves server | +| **Use when** | Public-facing upload widgets | Server pipelines, sensitive apps | + +**Key rule:** If you need `overwrite: true`, or need to pass parameters not in the unsigned whitelist, use a signed upload. + +## `resource_type` — Set This Explicitly + +**Default is `image`.** Cloudinary will attempt to process any upload as an image unless you override. + +| File type | Required `resource_type` | +|---|---| +| JPEG, PNG, GIF, WebP, SVG, etc. | `image` (default — still set explicitly) | +| MP4, MOV, AVI, WebM, etc. | `video` | +| PDF, ZIP, audio files, etc. | `raw` | +| Unknown or mixed | `auto` (Cloudinary detects) | + +**Failure modes when wrong:** +- Video uploaded as `image` → Cloudinary tries image processing → error or corrupted result +- Raw file uploaded as `image` → rejected or misprocessed +- Audio file → use `video` resource_type (Cloudinary processes audio under video) + +**Recommendation:** Use `auto` when file type is not known in advance. Use specific types when you control the upload. + +## Upload Presets + +Upload presets define default parameters for uploads. Configure at: **Cloudinary Console → Settings → Upload → Upload presets**. + +**For unsigned uploads:** Preset is required. Preset defines the parameters that can't be passed at request time. + +**For signed uploads:** Preset is optional but useful for shared defaults across upload calls. + +### Parameter Precedence + +**Signed uploads:** Request parameters override preset parameters. Exception: `eager` and `incoming_transformation` are merged, not overridden. + +**Unsigned uploads:** Request parameters (from the allowed whitelist) override the corresponding preset values. Parameters not in the whitelist are controlled entirely by the preset and cannot be overridden from the request. + +### Common preset settings to configure +- `folder` — target folder for organized asset management +- `allowed_formats` — restrict accepted file types +- `eager` — transformations to generate immediately on upload +- `tags` — default tags applied to all uploads +- `moderation` — manual or AI moderation pipeline +- `auto_tagging` — AI tagging confidence threshold (0.0–1.0) + +## Upload Parameters + +### Unsigned upload — allowed parameters at request time + +Only these parameters may be passed in an unsigned upload request. All others must be set in the upload preset: + +``` +upload_preset (required) +public_id (custom asset name/path) +folder (target folder) +tags (comma-separated list) +context (key=value metadata pairs) +metadata (structured metadata) +face_coordinates (manual face bounding boxes) +custom_coordinates (manual focus area) +regions (named regions) +filename_override (override original filename) +``` + +Any parameter not in this list is silently ignored in unsigned uploads. Move it to the upload preset instead. + +### `public_id` — common traps + +- **Whitespace in `public_id`** → silently ignored → Cloudinary assigns a random ID +- **Null or empty `public_id`** → random UUID assigned +- **File extension in `public_id`** → extension is stored as part of the ID, not stripped +- Once assigned, `public_id` cannot be changed without re-uploading or using the rename API + +### `format` vs `allowed_formats` interaction + +- **`allowed_formats`** validates the incoming file type. If the file type is in this list, it is stored as-is (no conversion). +- **`format`** converts files to the specified format — but only for files **not** in `allowed_formats`. +- If a file type is in `allowed_formats`, the `format` parameter is ignored for that file. + +**Example:** `allowed_formats: [jpg, png]`, `format: webp` +- Upload a JPG → stored as JPG (not converted to WebP) +- Upload a BMP → converted to WebP + +## Large Files + +**Threshold:** Files > 100 MB require chunked upload. + +**Minimum chunk size:** 5 MB (except the final chunk, which can be smaller). + +**How it works:** +1. Split file into chunks of ≥ 5 MB +2. Send each chunk with `Content-Range` header and a consistent `X-Unique-Upload-Id` header +3. Cloudinary returns `done: false` for intermediate chunks — handle this response, do not treat as error +4. Final chunk response contains the full upload result + +**Content-Range format:** +``` +bytes -/ +``` +- Range is **inclusive** on both ends +- First chunk of 6 MB: `bytes 0-5999999/22744222` (6,000,000 bytes) +- Off-by-one errors here cause rejected chunks + +For full Content-Range arithmetic and request structure, see [references/chunked-uploads.md](references/chunked-uploads.md). + +**SDK note:** Most SDKs handle chunking automatically above their threshold. Check your SDK's upload method for large-file variants (e.g. Python's `upload_large()` for files > 100 MB). + +## Remote/Fetch Upload + +Upload an asset directly from a public URL without downloading it first. + +**Request:** +``` +POST https://api.cloudinary.com/v1_1///upload + type: fetch + file: + api_key: + timestamp: + signature: +``` + +**URL rules:** +- Maximum 255 characters +- Must be URL-encoded (spaces → `%20`, special chars → `%XX`) +- URL must be publicly accessible (no auth required) +- Remote server timeouts apply if asset is large or slow + +**Signed fetch:** If your account has fetch URL restrictions enabled, the fetch URL itself must be signed. See your account security settings. + +## Signed Upload Signature + +Signatures authenticate server-side upload requests. **The API secret must never appear in client-side code.** + +**What to include in the signature string:** +- All request parameters **except**: `file`, `cloud_name`, `resource_type`, `api_key` +- Do not include `signature` itself + +**How to generate:** +1. Collect all upload parameters (excluding the four above) +2. Sort parameters alphabetically by key +3. Join as `key=value` pairs with `&` between them +4. Append your API secret directly (no separator): `sorted_params_stringYOUR_API_SECRET` +5. SHA-1 or SHA-256 hash the result + +**Timestamp rules:** +- Must be Unix timestamp in **seconds**, not milliseconds +- JavaScript: `Math.floor(Date.now() / 1000)` — `Date.now()` returns ms, divide by 1000 +- Signature expires 1 hour after the timestamp + +**Example parameter string (before hashing):** +``` +folder=uploads&public_id=my_image×tamp=1718100000YOUR_API_SECRET +``` + +For complete algorithm with edge cases and examples, see [references/signed-uploads.md](references/signed-uploads.md). + +## Security + +### API secret +- **Never** include `api_secret` in client-side code, browser requests, or mobile apps +- **Never** commit to version control (check `.env` files, config files) +- **Never** log it — check logging middleware and error handlers +- If exposed: rotate immediately in Cloudinary Console → Settings → Security → Access Keys + +### Unsigned preset exposure +- The upload preset name is visible in browser network requests and source code +- Attackers can discover and reuse it to upload to your account (quota abuse) +- Unsigned uploads cannot overwrite existing assets — this limits damage +- Use signed uploads for sensitive applications or when upload volume abuse is a concern + +### Checklist before going live +- API secret is server-side only +- Upload preset is set to unsigned only if truly needed client-side +- `allowed_formats` restricts file types to what your app expects +- `max_bytes` set in preset to prevent oversized uploads + +## Async Uploads + +Set `async: true` to process uploads in the background. Useful for large files or expensive eager transformations. + +**Response when async:** Only contains `{status: "pending", batch_id: "..."}`. The full upload result is **not** in this response. + +**Full result delivery:** Cloudinary POSTs the result to `notification_url` when processing completes. + +**Required:** Set `notification_url` in your request or upload preset when using `async: true`. Without it, the result is lost. + +**Python SDK:** `async` is a reserved keyword. Pass it as a dictionary key: +```python +# Wrong — syntax error +cloudinary.uploader.upload("file.jpg", async=True) + +# Correct +cloudinary.uploader.upload("file.jpg", **{"async": True}) +# or +cloudinary.uploader.upload("file.jpg", notification_url="https://...", **{"async": True}) +``` + +## Generate + Validate Checklist + +**After generating any upload configuration or code, verify all of the following before returning:** + +1. ✅ **`resource_type` explicitly set** — not relying on default `image`; matches the actual file type +2. ✅ **Unsigned param list valid** — request only contains parameters from the unsigned whitelist; everything else is in the preset +3. ✅ **`public_id` has no whitespace** — leading/trailing spaces cause silent fallback to random ID +4. ✅ **Signature timestamp in seconds** — not milliseconds (`Math.floor(Date.now() / 1000)` in JS) +5. ✅ **API secret not in client code** — only present in server-side signature generation +6. ✅ **`notification_url` set when `async: true`** — without it, upload result is lost +7. ✅ **`overwrite: true` not expected in unsigned flow** — silently forced to `false` +8. ✅ **Large file (> 100 MB) → chunked upload** — plain upload will fail or time out + +## Debugging Workflow + +When an upload fails or behaves unexpectedly, follow these steps in order: + +### Step 1: Read the error response +Cloudinary errors are in the response body: +```json +{"error": {"message": "..."}} +``` +Note the exact message before doing anything else. See [references/troubleshooting.md](references/troubleshooting.md) for error message → fix mappings. + +### Step 2: Check `resource_type` +Is the `resource_type` in the URL correct for the file being uploaded? +- URL contains `/image/upload/` but file is a video → change to `/video/upload/` +- Use `/auto/upload/` if file type varies + +### Step 3: Check upload type (signed vs unsigned) +- **Unsigned:** Is `upload_preset` in the request? Is it spelled correctly and set to unsigned in the Console? +- **Signed:** Is `api_key`, `timestamp`, and `signature` all present? Is timestamp in seconds? + +### Step 4: Check parameter validity +- **Unsigned:** Are any non-whitelisted parameters in the request? Remove them or move to preset. +- **All:** Is `public_id` free of whitespace? + +### Step 5: Verify the signature (signed uploads only) +1. Did you exclude `file`, `cloud_name`, `resource_type`, `api_key` from the signature string? +2. Are parameters sorted alphabetically before joining? +3. Is the API secret appended directly with no separator? +4. Is the timestamp within the last hour? + +See [references/signed-uploads.md](references/signed-uploads.md) for the full algorithm. + +### Step 6: Check preset configuration +Open Cloudinary Console → Settings → Upload → [your preset] and verify: +- Preset mode matches usage (signed vs unsigned) +- `allowed_formats` isn't blocking the file type +- `max_bytes` limit isn't exceeded + +### Step 7: Check chunked upload (large files only) +- Is `Content-Range` format correct: `bytes -/`? +- Is the range inclusive on both ends? +- Is `X-Unique-Upload-Id` the same value across all chunks? +- Are all chunks except the last ≥ 5 MB? + +See [references/chunked-uploads.md](references/chunked-uploads.md) for arithmetic details. + +### Step 8: Remote/fetch upload checks +- Is the URL ≤ 255 characters? +- Are special characters URL-encoded? +- Is the URL publicly accessible without authentication? + +## Related Skills + +- **cloudinary-next** and **cloudinary-react** — framework-specific upload wiring: API routes and server actions, upload widget components, and environment variable handling. +- **cloudinary-docs** — anything outside this skill's scope, looked up in the current Cloudinary documentation. + +## Additional Resources + +- [references/signed-uploads.md](references/signed-uploads.md) — Full signature algorithm with examples +- [references/chunked-uploads.md](references/chunked-uploads.md) — Content-Range arithmetic, chunk requirements +- [references/troubleshooting.md](references/troubleshooting.md) — Error messages and fixes +- [Upload API Reference](https://cloudinary.com/documentation/image_upload_api_reference.md) +- [Upload Images Documentation](https://cloudinary.com/documentation/upload_images.md) +- [Authentication Signatures](https://cloudinary.com/documentation/authentication_signatures.md) +- [Upload Presets](https://cloudinary.com/documentation/upload_presets.md) diff --git a/skills/platform/cloudinary-upload/references/chunked-uploads.md b/skills/platform/cloudinary-upload/references/chunked-uploads.md new file mode 100644 index 0000000..d7e296d --- /dev/null +++ b/skills/platform/cloudinary-upload/references/chunked-uploads.md @@ -0,0 +1,97 @@ +# Chunked Uploads — Large File Reference + +Reference for uploading files larger than 100 MB using the chunked upload API. + +## When to Use + +- Files > 100 MB +- Unreliable network connections where resumable uploads help +- Progress tracking on large uploads + +## Required Headers + +Every chunk request must include: + +| Header | Value | Notes | +|---|---|---| +| `Content-Range` | `bytes -/` | See arithmetic below | +| `X-Unique-Upload-Id` | Any unique string | Same value for all chunks of one file | +| `Content-Type` | `multipart/form-data` | Standard upload content type | + +## Content-Range Arithmetic + +**Format:** `bytes -/` + +- `start`: byte offset of first byte in this chunk (0-indexed) +- `end`: byte offset of **last byte** in this chunk (inclusive — this is where off-by-one errors happen) +- `total`: total file size in bytes + +**Example — 22,744,222 byte file split into 6 MB chunks:** + +| Chunk | Start | End | Total | Header | +|---|---|---|---|---| +| 1 | 0 | 5,999,999 | 22,744,222 | `bytes 0-5999999/22744222` | +| 2 | 6,000,000 | 11,999,999 | 22,744,222 | `bytes 6000000-11999999/22744222` | +| 3 | 12,000,000 | 17,999,999 | 22,744,222 | `bytes 12000000-17999999/22744222` | +| 4 | 18,000,000 | 22,744,221 | 22,744,222 | `bytes 18000000-22744221/22744222` | + +**Key:** end = start + chunk_size - 1 (not start + chunk_size) + +## Chunk Size Rules + +- **Minimum chunk size:** 5 MB (5,242,880 bytes) — enforced for all chunks except the last +- **Maximum chunk size:** No hard limit, but 20 MB is a practical ceiling for reliability +- **Last chunk:** Can be any size (even 1 byte) + +## X-Unique-Upload-Id + +- Generate once per file upload (UUID or similar) +- Use the **same value** for every chunk of that file +- Cloudinary uses this to reassemble chunks on its end +- Generating a new ID mid-upload = Cloudinary treats it as a new upload + +## Response Handling + +| Chunk | Response | Meaning | +|---|---|---| +| Intermediate | `{"done": false}` | Chunk received, waiting for more | +| Final | Full upload result object | Upload complete | + +Do not treat `{"done": false}` as an error. Track chunk index and continue sending. + +## Chunking Algorithm + +``` +chunk_size = 6 * 1024 * 1024 # 6 MB +upload_id = generate_uuid() +total_size = file.size +offset = 0 + +while offset < total_size: + chunk_end = min(offset + chunk_size, total_size) - 1 + chunk_data = file.read(offset, chunk_end + 1) + + headers = { + "Content-Range": f"bytes {offset}-{chunk_end}/{total_size}", + "X-Unique-Upload-Id": upload_id + } + + response = post("/upload", data=chunk_data, headers=headers) + + if response["done"]: + return response # final result + else: + offset = chunk_end + 1 +``` + +## Common Mistakes + +| Mistake | Symptom | Fix | +|---|---|---| +| Off-by-one in `end` (using `start + size` instead of `start + size - 1`) | `400 Invalid Content-Range` | end = start + chunk_size - 1 | +| Different `X-Unique-Upload-Id` per chunk | Upload never completes; new upload started | Generate once, reuse for all chunks | +| Chunk size < 5 MB (not final chunk) | `400 Chunk too small` | Enforce 5 MB minimum for all but last chunk | +| Not handling `done: false` | Treating mid-upload as error | Only treat HTTP errors as failures | +| Wrong `total` in Content-Range | `400 Invalid Content-Range` | Use actual file byte size, not estimated | + +Official documentation: [Manual Chunked Upload](https://cloudinary.com/documentation/upload_images.md#manual_chunked_upload_rest) diff --git a/skills/platform/cloudinary-upload/references/signed-uploads.md b/skills/platform/cloudinary-upload/references/signed-uploads.md new file mode 100644 index 0000000..deaa5dc --- /dev/null +++ b/skills/platform/cloudinary-upload/references/signed-uploads.md @@ -0,0 +1,79 @@ +# Signed Uploads — Signature Generation + +Complete reference for generating Cloudinary upload signatures. + +## The Algorithm + +1. **Collect parameters** — all upload request parameters except: + - `file` + - `cloud_name` + - `resource_type` + - `api_key` + - `signature` (obviously) + +2. **Sort alphabetically** by parameter key (a–z) + +3. **Build the parameter string** — join as `key=value` pairs with `&`: + ``` + folder=uploads&public_id=my_asset×tamp=1718100000&tags=product,sale + ``` + +4. **Append API secret** — append directly, no `&`, no `=`, no separator: + ``` + folder=uploads&public_id=my_asset×tamp=1718100000&tags=product,saleMY_API_SECRET + ``` + +5. **Hash it** — SHA-1 or SHA-256 (SHA-256 recommended for new implementations): + ``` + signature = sha256(parameter_string + api_secret) + ``` + +## Timestamp Rules + +- Must be **Unix time in seconds** (not milliseconds) +- JavaScript pitfall: `Date.now()` → milliseconds → divide by 1000: + ```javascript + const timestamp = Math.floor(Date.now() / 1000); + ``` +- Python: `int(time.time())` returns seconds natively +- Signature is valid for **1 hour** from the timestamp value +- Expired signatures return: `{"error": {"message": "Signature has expired"}}` + +## Multi-Value Parameters + +Some parameters accept multiple values (e.g. `tags`, `eager`). Join with `|` before including in signature string: +``` +tags=product|sale|featured +eager=c_fill,w_400|c_scale,w_200 +``` + +> **Note:** Tags are comma-separated in the request body (e.g. `tags: "product,sale"`) but joined with `|` only within the signature string. These are different contexts — don't confuse them. + +## Upload Preset in Signature + +If using an upload preset in a signed upload, include `upload_preset` in the signature string. Since `t` comes before `u` alphabetically, `timestamp` sorts before `upload_preset`: +``` +timestamp=1718100000&upload_preset=my_presetMY_API_SECRET +``` + +## Common Mistakes + +| Mistake | Symptom | Fix | +|---|---|---| +| Timestamp in milliseconds | `Signature has expired` immediately | Divide by 1000 in JS | +| Including `file` in signature | `Invalid signature` | Exclude `file` from signature params | +| Including `api_key` in signature | `Invalid signature` | Exclude `api_key` from signature params | +| Wrong sort order | `Invalid signature` | Sort keys a–z strictly | +| Missing parameter in signature | `Invalid signature` | Include ALL request params except the four excluded ones | +| Parameter in request but not signature | `Invalid signature` | Any param sent in the request must be in the signature | +| Wrong API secret | `Invalid signature` | Verify you're using the correct secret for the cloud | + +## Verification + +To debug a signature mismatch: +1. Log the exact parameter string before hashing (temporarily) +2. Verify each parameter appears in both the string and the request +3. Verify sort order: compare your sorted keys to alphabetical +4. Verify timestamp value is ≤ 3600 seconds in the past + +Official documentation: [Authentication Signatures](https://cloudinary.com/documentation/authentication_signatures.md) diff --git a/skills/platform/cloudinary-upload/references/troubleshooting.md b/skills/platform/cloudinary-upload/references/troubleshooting.md new file mode 100644 index 0000000..afa19c9 --- /dev/null +++ b/skills/platform/cloudinary-upload/references/troubleshooting.md @@ -0,0 +1,70 @@ +# Upload Troubleshooting + +Error messages, causes, and fixes for Cloudinary upload failures. + +## Error Response Format + +Cloudinary returns errors as JSON: +```json +{"error": {"message": "Exact error message here"}} +``` +HTTP status is typically 400 (bad request) or 401 (auth failure). + +## Error Reference + +### Authentication & Signature Errors + +| Error message | Root cause | Fix | +|---|---|---| +| `Invalid signature` | Signature mismatch | Check: parameter sort order, excluded params (file, cloud_name, resource_type, api_key), API secret is correct. See [signed-uploads.md](signed-uploads.md) | +| `Signature has expired` | Timestamp > 1 hour old, or timestamp in milliseconds | Use `Math.floor(Date.now() / 1000)` in JS; regenerate signature | +| `Missing required parameter - signature` | Signed upload missing signature field | Add `signature` to request | +| `Missing required parameter - timestamp` | Signed upload missing timestamp field | Add `timestamp` (seconds) to request | +| `Unknown API key` | Wrong or missing `api_key` | Verify api_key in Cloudinary Console → Settings → Security | + +### Upload Preset Errors + +| Error message | Root cause | Fix | +|---|---|---| +| `Upload preset must be specified` | Unsigned upload missing `upload_preset` | Add `upload_preset` parameter | +| `Upload preset not found` | Preset name is wrong or doesn't exist | Check preset name in Console → Settings → Upload | +| `Upload preset is not enabled for unsigned uploads` | Preset is set to signed mode | Change preset mode to unsigned, or use signed upload | + +### Resource & File Errors + +| Error message | Root cause | Fix | +|---|---|---| +| `Invalid image file` | Wrong `resource_type` for uploaded file | Set `resource_type: "video"` for videos, `"raw"` for non-media files | +| `File size too large` | Exceeds `max_bytes` in preset | Increase preset limit or reduce file size | +| `Invalid file type` | File extension not in `allowed_formats` | Add format to preset's `allowed_formats`, or use a different file | +| `Resource not found` | Wrong `public_id` or asset doesn't exist | Check `public_id` for whitespace; verify asset exists | + +### Chunked Upload Errors + +| Error message | Root cause | Fix | +|---|---|---| +| `Invalid Content-Range header` | Wrong format or arithmetic | Use `bytes start-end/total`; end = start + chunk_size - 1 | +| `Chunk too small` | Non-final chunk < 5 MB | Ensure all chunks except last are ≥ 5,242,880 bytes | + +### Account & Quota Errors + +| Error message | Root cause | Fix | +|---|---|---| +| `Account is not active` | New account not email-verified | Verify email address in Cloudinary Console | +| `Quota exceeded` | Storage or bandwidth limit reached | Upgrade plan or delete unused assets | +| `Transformation credits exceeded` | Eager transformations over quota | Reduce eager transforms or upgrade plan | + +## Silent Failures (No Error, Wrong Behavior) + +| Behavior | Root cause | Fix | +|---|---|---| +| Asset gets random public_id instead of expected | `public_id` has whitespace, or null/empty | Strip whitespace from `public_id` before upload | +| Extra parameters ignored in unsigned upload | Parameters not in unsigned whitelist | Move parameters to upload preset | +| `overwrite: true` has no effect | Forced `false` for unsigned uploads | Switch to signed upload if overwrite is needed | +| Format not converted as expected | File type is in `allowed_formats` — stored as-is | Remove file type from `allowed_formats` if conversion is needed | +| Async upload result never arrives | `notification_url` not set | Add `notification_url` to request or preset | +| Video transformation returns still image | Used `f_auto` instead of `f_auto:video` | Use `f_auto:video` for video outputs (handled by cloudinary-transformations skill) | + +## Debugging Checklist + +If the above table doesn't match your error, run through the debugging workflow in [SKILL.md](../SKILL.md#debugging-workflow). From 750472b0098b9b209c1d4065f67ebe4c770a62a5 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Mon, 14 Sep 2026 10:36:35 +0100 Subject: [PATCH 6/6] Address first-round review on cloudinary-upload skill Make the skill opinionated about best practices rather than a docs restatement, per PR #21 review: - Lead with default best practices; recommend upload presets for any repeated upload pattern and list recommended preset settings - Add Transformations on Upload: eager vs incoming, no f_auto at upload time, delivery string must match eager, named transformations - Add Naming and Folders: public_id strategies, use_filename, asset_folder vs folder by folder mode, display_name, overwrite - Add SDK vs Upload widget vs REST guidance and a widget snippet - Add install_source and referrer params to every documentation link - Align structure and voice with the other skills (self-validation checklist, common mistakes, progressive-disclosure resources) and the CONTRIBUTING description shape Correct facts checked against the docs: unsigned precedence favours the preset, the unsigned allow-list was incomplete, and remote upload does not use type=fetch. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- skills/platform/cloudinary-upload/SKILL.md | 402 ++++++++++++------ .../references/chunked-uploads.md | 2 +- .../references/signed-uploads.md | 2 +- .../references/troubleshooting.md | 7 +- 5 files changed, 292 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index f99998f..e59d5d2 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Skills are grouped by category below. The category is a folder in this repo only |---|---| | `cloudinary-docs` | Selects the most relevant markdown pages from the current documentation using the latest llms.txt. Use when answering Cloudinary questions or integrating Cloudinary into code. | | `cloudinary-transformations` | Turns natural language image and video transformation requirements into valid URL transformation strings that follow Cloudinary best practices. Use when building delivery URLs, applying transformations, optimizing media, or debugging transformation syntax errors. | -| `cloudinary-upload` | Guide uploading assets to Cloudinary — signed and unsigned uploads, upload presets, large files, remote/fetch uploads, and signature generation. Use when uploading files or URLs to Cloudinary, configuring upload presets, generating upload signatures, or debugging upload failures. For Next.js or React projects, use cloudinary-next or cloudinary-react alongside this skill. | +| `cloudinary-upload` | Reference for uploading assets to Cloudinary, covering signed and unsigned uploads, upload presets, transformations on upload, naming and folders, large files, remote uploads, and signature generation. Use when uploading files or URLs to Cloudinary, configuring upload presets, generating upload signatures, choosing between the SDK, REST API, or Upload widget, or debugging upload failures. For Next.js or React projects, use cloudinary-next or cloudinary-react alongside this skill. | ### Frameworks diff --git a/skills/platform/cloudinary-upload/SKILL.md b/skills/platform/cloudinary-upload/SKILL.md index 4b1b022..ac1430b 100644 --- a/skills/platform/cloudinary-upload/SKILL.md +++ b/skills/platform/cloudinary-upload/SKILL.md @@ -1,6 +1,6 @@ --- name: cloudinary-upload -description: Guide uploading assets to Cloudinary — signed and unsigned uploads, upload presets, large files, remote/fetch uploads, and signature generation. Use when uploading files or URLs to Cloudinary, configuring upload presets, generating upload signatures, or debugging upload failures. For Next.js or React projects, use cloudinary-next or cloudinary-react alongside this skill. +description: Reference for uploading assets to Cloudinary, covering signed and unsigned uploads, upload presets, transformations on upload, naming and folders, large files, remote uploads, and signature generation. Use when uploading files or URLs to Cloudinary, configuring upload presets, generating upload signatures, choosing between the SDK, REST API, or Upload widget, or debugging upload failures. For Next.js or React projects, use cloudinary-next or cloudinary-react alongside this skill. license: MIT metadata: author: cloudinary @@ -11,31 +11,73 @@ metadata: ## When to Use -- Uploading images, videos, or raw files to Cloudinary from any SDK or the REST API +- Uploading images, videos, or raw files to Cloudinary from any SDK, the REST API, or the Upload widget +- Choosing how to upload: backend SDK, Upload widget, or direct REST call +- Creating or configuring upload presets (signed or unsigned) +- Applying transformations at upload time (eager or incoming) +- Deciding how uploaded assets are named and organized into folders - Generating upload signatures for signed uploads -- Configuring upload presets (signed or unsigned) +- Uploading large files (> 100 MB) or from remote URLs - Debugging upload failures (error responses, silent failures, unexpected behavior) -- Setting up remote/fetch uploads from external URLs -- Uploading large files (> 100 MB) ## Quick Start +### Default Best Practices: Apply These to Every Upload + +Unless the user has a specific reason not to, every upload configuration you generate should: + +1. **Use an upload preset.** Whenever more than one asset will be uploaded with the same parameters, put those parameters in a preset and pass only `upload_preset` in the request. See [Upload Presets](#upload-presets). +2. **Set `resource_type` explicitly** (`image`, `video`, `raw`, or `auto`). Never rely on the default. See [`resource_type`](#resource_type--set-this-explicitly). +3. **Decide naming deliberately.** Either set a deterministic `public_id`, or set `use_filename: true`. Never let production assets fall back to random IDs by accident. See [Naming and Folders](#naming-and-folders). +4. **Restrict what can be uploaded**: `allowed_formats` and `max_bytes` in the preset, especially for unsigned uploads. +5. **Pre-generate derived assets with `eager`** when the delivery transformations are known up front, and use `eager_async: true` for video. See [Transformations on Upload](#transformations-on-upload). +6. **Keep the API secret server-side.** Signed uploads run on the server; browsers use unsigned presets or a server-issued signature. + +**Important:** The parameter values shown throughout this skill are illustrative. Choose preset names, folders, formats, and transformations based on the user's actual requirements, not the example values. + +### Choosing the Upload Method + +| Method | Use when | Why | +|---|---|---| +| **Backend SDK** (Node, Python, Ruby, PHP, Java, .NET, Go) | Server-side uploads in a language Cloudinary supports | Handles signature generation, chunked uploads for large files, and response validation for you. **Default choice for server code.** | +| **Upload widget** | Users upload from a browser and need a UI | Complete drop-in UI (drag and drop, camera, URL, social and stock sources, cropping, progress). Works unsigned with a preset or signed via your backend. | +| **REST API directly** | No SDK for the language, or a minimal client-side unsigned upload without the widget | Full control, but you write the multipart request, signature, chunking, and error handling yourself. | + +**Recommendation:** Prefer the SDK on the server and the Upload widget in the browser. Fall back to raw REST only when neither fits. For React and Next.js widget wiring, use the cloudinary-react or cloudinary-next skill. + +### Request Shapes + **Unsigned upload (client-side, browser):** ``` POST https://api.cloudinary.com/v1_1///upload - upload_preset: + upload_preset: file: ``` **Signed upload (server-side only):** ``` POST https://api.cloudinary.com/v1_1///upload + upload_preset: (recommended) api_key: timestamp: signature: file: ``` +**Upload widget (browser, unsigned):** +```html + + +``` + **Remote/fetch upload (signed, server-side):** ``` POST https://api.cloudinary.com/v1_1///upload @@ -43,97 +85,185 @@ POST https://api.cloudinary.com/v1_1///upload timestamp: signature: file: - type: fetch ``` -> **Note:** Replace `` with `image`, `video`, or `raw` — never omit it or rely on a default. See the `resource_type` section below. +> **Note:** Replace `` with `image`, `video`, `raw`, or `auto`. Never omit it or rely on a default. ## Signed vs Unsigned | | Unsigned | Signed | |---|---|---| | **Who runs it** | Client (browser, mobile) | Server only | -| **Auth** | Upload preset name | API key + secret signature | -| **Upload preset** | Required | Optional (recommended for defaults) | -| **Parameter control** | Restricted whitelist only | Full | +| **Auth** | Unsigned upload preset name | API key + secret signature | +| **Upload preset** | Required | Optional, but recommended | +| **Parameter control** | Restricted allow-list in the request; everything else comes from the preset | Full | +| **Precedence when a param is in both** | Preset wins (with exceptions, see below) | Request wins (with exceptions, see below) | | **Overwrite** | Always forced to `false` | Configurable | -| **Security** | Preset name visible in requests | API secret never leaves server | -| **Use when** | Public-facing upload widgets | Server pipelines, sensitive apps | +| **Security** | Preset name visible in requests | API secret never leaves the server | +| **Use when** | Public-facing upload UI, user-generated content | Server pipelines, migrations, anything needing overwrite or restricted params | -**Key rule:** If you need `overwrite: true`, or need to pass parameters not in the unsigned whitelist, use a signed upload. - -## `resource_type` — Set This Explicitly +**Key rule:** If you need `overwrite: true`, or need to pass parameters outside the unsigned allow-list at request time, use a signed upload. -**Default is `image`.** Cloudinary will attempt to process any upload as an image unless you override. +## Upload Presets -| File type | Required `resource_type` | -|---|---| -| JPEG, PNG, GIF, WebP, SVG, etc. | `image` (default — still set explicitly) | -| MP4, MOV, AVI, WebM, etc. | `video` | -| PDF, ZIP, audio files, etc. | `raw` | -| Unknown or mixed | `auto` (Cloudinary detects) | +An upload preset is a named, centrally managed set of upload parameters. Pass `upload_preset: ` and every parameter in the preset is applied to the upload. -**Failure modes when wrong:** -- Video uploaded as `image` → Cloudinary tries image processing → error or corrupted result -- Raw file uploaded as `image` → rejected or misprocessed -- Audio file → use `video` resource_type (Cloudinary processes audio under video) +**Recommend a preset whenever more than one asset will be uploaded with the same parameters.** Treat request-time parameters as the exception, not the default. Presets: -**Recommendation:** Use `auto` when file type is not known in advance. Use specific types when you control the upload. +- Keep upload behavior consistent across every SDK, widget, and REST call in the project +- Let the team change folders, transformations, moderation, or limits in the Console without redeploying code +- Are the only way to control non-allow-listed parameters for unsigned uploads, and the only way to lock them down +- Make the security posture auditable: one place to check `allowed_formats`, `max_bytes`, and folder placement -## Upload Presets +Configure at **Cloudinary Console → Settings → Upload → Upload presets**, or programmatically with the Admin API `upload_presets` methods. -Upload presets define default parameters for uploads. Configure at: **Cloudinary Console → Settings → Upload → Upload presets**. +### Recommended Preset Settings -**For unsigned uploads:** Preset is required. Preset defines the parameters that can't be passed at request time. +Proactively suggest these when creating a preset; ask about the ones that depend on the use case: -**For signed uploads:** Preset is optional but useful for shared defaults across upload calls. +| Setting | Why | +|---|---| +| `asset_folder` (dynamic folder mode) or `folder` (fixed folder mode) | Organizes uploads. See [Naming and Folders](#naming-and-folders) for which one applies. | +| `use_filename: true` + `unique_filename` | Readable public IDs derived from the original filename, with or without a random suffix. | +| `allowed_formats` | Rejects unexpected file types. Essential for unsigned presets. | +| `max_bytes` | Caps file size. Essential for unsigned presets. | +| `eager` (+ `eager_async: true` for video) | Pre-generates the derived assets your app will deliver. See [Transformations on Upload](#transformations-on-upload). | +| `transformation` (incoming) | Normalizes user-generated content before storage (cap dimensions, clip duration). | +| `tags`, `context`, `metadata` | Default classification applied to every upload through the preset. | +| `moderation`, `categorization`, `auto_tagging` | Add-on pipelines for user-generated content. | +| `notification_url` | Webhook for async results and moderation outcomes. | + +Name presets by purpose (`user-avatars`, `product-images`, `video-social`), not by who created them. ### Parameter Precedence -**Signed uploads:** Request parameters override preset parameters. Exception: `eager` and `incoming_transformation` are merged, not overridden. +When the same parameter is in both the preset and the request: -**Unsigned uploads:** Request parameters (from the allowed whitelist) override the corresponding preset values. Parameters not in the whitelist are controlled entirely by the preset and cannot be overridden from the request. +**Signed uploads:** the **request** value wins, except `eager` and incoming `transformation`, which are **merged** (both sets applied). -### Common preset settings to configure -- `folder` — target folder for organized asset management -- `allowed_formats` — restrict accepted file types -- `eager` — transformations to generate immediately on upload -- `tags` — default tags applied to all uploads -- `moderation` — manual or AI moderation pipeline -- `auto_tagging` — AI tagging confidence threshold (0.0–1.0) +**Unsigned uploads:** the **preset** value wins, except: +- `context` and `metadata` are **merged** +- `public_id` and `filename_override` come from the **request** (unless the preset sets `disallow_public_id: true`) -## Upload Parameters +**Important:** This means an unsigned request cannot override the preset's `folder`, `asset_folder`, or `tags`. If a value must vary per upload in an unsigned flow, either leave it out of the preset or use a signed upload. -### Unsigned upload — allowed parameters at request time +## Transformations on Upload -Only these parameters may be passed in an unsigned upload request. All others must be set in the upload preset: +Cloudinary generates transformations lazily on first delivery request by default. Two upload-time options change that: + +| | `eager` | `transformation` (incoming) | +|---|---|---| +| **What it does** | Generates derived versions **in addition to** the stored original | Modifies the original **before** it is stored | +| **Original preserved?** | Yes | No, the transformed result is the original | +| **Use for** | Warming the cache so first visitors get instant delivery; video transformations; AI or add-on transformations that are slow; strict transformations mode | Normalizing user-generated content: cap resolution, clip video duration, strip metadata, reduce storage cost | +| **Default recommendation** | **Yes**, when the delivery transformations are known up front | Only when you genuinely want to discard the original | + +**Rules:** + +- **Prefer eager.** Keep originals unless there is a storage or normalization reason to alter them. Never apply an incoming transformation to layered formats such as PSD or TIFF. +- **Do not put the same transformation in both** `eager` and `transformation`. Pick one per transformation. +- **Never use `f_auto` in eager or incoming transformations.** Format negotiation happens at delivery based on the requesting browser, so there is nothing to negotiate at upload time. Pre-generate each format explicitly in eager (for example one `f_webp` and one `f_jpg` variant), then deliver with `f_auto` and no file extension so `f_auto` resolves to one of the pre-generated variants. `q_auto` is fine in both. +- **Delivery URLs must match the eager transformation exactly.** Cloudinary treats `c_fill,h_400,w_600` and `c_fill,w_600,h_400` as different derived assets. If the delivery URL differs by parameter order, extension, or an added `f_auto`, the eager asset is wasted and a new one is generated lazily. Use the cloudinary-transformations skill to build the delivery URL and keep the two in sync. +- **Use named transformations for anything reused.** Define `t_` once, pass `eager: t_`, and deliver with `t_`. This guarantees the eager and delivery strings match and lets the team change the transformation without touching upload or delivery code. `f_auto` does not work inside a named transformation, so append it on the delivery URL: `t_/f_auto`. +- **Use `eager_async: true` with `eager_notification_url` for video** and other slow transformations, so the upload call returns immediately and Cloudinary notifies you when derived assets are ready. +- **REST syntax:** `eager` is a pipe-separated list of transformation strings; chained components use `/`. SDKs accept arrays of transformation hashes. ``` -upload_preset (required) -public_id (custom asset name/path) -folder (target folder) -tags (comma-separated list) -context (key=value metadata pairs) -metadata (structured metadata) -face_coordinates (manual face bounding boxes) -custom_coordinates (manual focus area) -regions (named regions) -filename_override (override original filename) +eager: c_fill,g_auto,h_400,w_600,f_webp|c_fill,g_auto,h_400,w_600,f_jpg|t_product_card +eager_async: true +eager_notification_url: https://example.com/webhooks/cloudinary ``` -Any parameter not in this list is silently ignored in unsigned uploads. Move it to the upload preset instead. +## Naming and Folders + +### Choose the naming strategy + +| Strategy | Set | Result | +|---|---|---| +| Deterministic ID (recommended for app-managed assets) | `public_id: products/sku-1234` | Predictable URLs; combine with `overwrite: false` to skip duplicates or `overwrite: true, invalidate: true` to replace | +| Original filename | `use_filename: true` | `public_id` derived from the uploaded filename with a random suffix appended | +| Original filename, no suffix | `use_filename: true, unique_filename: false` | Exact filename as `public_id`; collisions are governed by `overwrite` | +| Random (default) | nothing | Random `public_id`. Acceptable for throwaway or user-generated content, not for SEO or predictable delivery | + +Set the strategy in the preset so every upload path agrees. `filename_override` stores an original-filename value different from the uploaded file's name and works with `use_filename`. + +### `public_id` rules and traps + +- Do not include a file extension for images or videos. Include it only for `raw` files. +- Up to 255 characters. Avoid `? & # \ % < > +`. Cannot begin or end with a space or `/`. +- **Whitespace or empty `public_id`** → silently ignored → Cloudinary assigns a random ID +- Once assigned, `public_id` cannot be changed without re-uploading or using the rename API, and changing it breaks existing delivery URLs -### `public_id` — common traps +### Dynamic vs fixed folder mode -- **Whitespace in `public_id`** → silently ignored → Cloudinary assigns a random ID -- **Null or empty `public_id`** → random UUID assigned -- **File extension in `public_id`** → extension is stored as part of the ID, not stripped -- Once assigned, `public_id` cannot be changed without re-uploading or using the rename API +Every product environment is in one of two folder modes. **All accounts created after June 2024 are dynamic.** Check **Console → Settings → Product environment** or the Admin API `config` method if unsure. + +| | Dynamic folder mode (default) | Fixed folder mode (legacy) | +|---|---|---| +| **Folder parameter** | `asset_folder` | `folder` | +| **Does the folder appear in the URL?** | No. `asset_folder` only controls Console placement. Add `public_id_prefix` (or `use_asset_folder_as_public_id_prefix: true`) if you also want the path in the `public_id` | Yes. `folder` is prepended to `public_id` and becomes part of the delivery URL | +| **Moving or renaming folders** | Safe, URLs unchanged | Changes `public_id` and breaks URLs | +| **Display name** | `display_name` or `use_filename_as_display_name: true` sets a human-friendly Console name independent of the URL | Not supported | + +**Rules:** +- In dynamic folder mode, **do not add `folder` to new code**. Use `asset_folder`, and `public_id_prefix` if the path should be in the URL. +- In dynamic folder mode, a `public_id` containing slashes does **not** place the asset in a folder. Set `asset_folder` too, or the asset lands in the root. +- `folder`, `asset_folder`, `public_id_prefix`, and `use_asset_folder_as_public_id_prefix` all belong in the preset for consistency. + +### Overwrite and duplicates + +- `overwrite` defaults to `false`. With a deterministic `public_id`, a repeat upload is skipped and the response includes the existing asset. This is the recommended way to avoid duplicates; no existence check is needed. +- To replace an asset in place: signed upload with `overwrite: true` and `invalidate: true` so CDN copies are refreshed. +- Unsigned uploads always treat `overwrite` as `false`. + +## `resource_type` — Set This Explicitly + +**Default is `image`.** Cloudinary will attempt to process any upload as an image unless you override. + +| File type | Required `resource_type` | +|---|---| +| JPEG, PNG, GIF, WebP, SVG, etc. | `image` (default — still set explicitly) | +| MP4, MOV, AVI, WebM, and audio (MP3, WAV, FLAC) | `video` | +| PDF, ZIP, text, and other non-media files | `raw` | +| Unknown or mixed | `auto` (Cloudinary detects) | + +**Failure modes when wrong:** +- Video uploaded as `image` → Cloudinary tries image processing → error or corrupted result +- Raw file uploaded as `image` → rejected or misprocessed +- Audio uploaded as `raw` → no audio transformations available; use `video` + +**Recommendation:** Use `auto` when the file type is not known in advance. Use specific types when you control the upload. + +## Unsigned Upload Parameters + +Only these parameters may be passed directly in an unsigned upload request. Everything else must come from the upload preset: + +``` +upload_preset (required) +public_id +public_id_prefix (dynamic folder mode only) +folder +asset_folder (dynamic folder mode only) +tags +context +metadata +face_coordinates +custom_coordinates +regions +source +filename_override +manifest_transformation +manifest_json +template +template_vars +``` + +Any other parameter in an unsigned request is silently ignored. Move it to the upload preset. Remember that for most of these the **preset value wins** if both are set (see [Parameter Precedence](#parameter-precedence)). ### `format` vs `allowed_formats` interaction - **`allowed_formats`** validates the incoming file type. If the file type is in this list, it is stored as-is (no conversion). -- **`format`** converts files to the specified format — but only for files **not** in `allowed_formats`. +- **`format`** converts files to the specified format, but only for files **not** in `allowed_formats`. - If a file type is in `allowed_formats`, the `format` parameter is ignored for that file. **Example:** `allowed_formats: [jpg, png]`, `format: webp` @@ -144,13 +274,15 @@ Any parameter not in this list is silently ignored in unsigned uploads. Move it **Threshold:** Files > 100 MB require chunked upload. +**SDK first:** Every backend SDK handles chunking for you (for example Python's `upload_large()`, Node's `upload_large()`). Prefer the SDK method over hand-rolled chunking whenever an SDK is available. + **Minimum chunk size:** 5 MB (except the final chunk, which can be smaller). -**How it works:** -1. Split file into chunks of ≥ 5 MB -2. Send each chunk with `Content-Range` header and a consistent `X-Unique-Upload-Id` header -3. Cloudinary returns `done: false` for intermediate chunks — handle this response, do not treat as error -4. Final chunk response contains the full upload result +**How it works (REST):** +1. Split the file into chunks of ≥ 5 MB +2. Send each chunk with a `Content-Range` header and a consistent `X-Unique-Upload-Id` header +3. Cloudinary returns `done: false` for intermediate chunks. Handle this response; do not treat it as an error +4. The final chunk response contains the full upload result **Content-Range format:** ``` @@ -162,33 +294,21 @@ bytes -/ For full Content-Range arithmetic and request structure, see [references/chunked-uploads.md](references/chunked-uploads.md). -**SDK note:** Most SDKs handle chunking automatically above their threshold. Check your SDK's upload method for large-file variants (e.g. Python's `upload_large()` for files > 100 MB). - ## Remote/Fetch Upload -Upload an asset directly from a public URL without downloading it first. - -**Request:** -``` -POST https://api.cloudinary.com/v1_1///upload - type: fetch - file: - api_key: - timestamp: - signature: -``` +Upload an asset directly from a public URL without downloading it first. Pass the URL as the `file` parameter of a normal (signed) upload; the asset is stored in your account like any other upload. **URL rules:** - Maximum 255 characters - Must be URL-encoded (spaces → `%20`, special chars → `%XX`) -- URL must be publicly accessible (no auth required) -- Remote server timeouts apply if asset is large or slow +- Must be publicly accessible (no auth required) +- Remote server timeouts apply if the asset is large or slow -**Signed fetch:** If your account has fetch URL restrictions enabled, the fetch URL itself must be signed. See your account security settings. +**Not the same as `type: fetch` delivery.** Fetch delivery URLs (`/image/fetch/`) proxy a remote asset on the fly without storing it as an upload. Use a remote upload when you want the asset in your Media Library with its own `public_id`. ## Signed Upload Signature -Signatures authenticate server-side upload requests. **The API secret must never appear in client-side code.** +Signatures authenticate server-side upload requests. **The API secret must never appear in client-side code.** If an SDK is available, let it sign the request; only implement this by hand for raw REST calls. **What to include in the signature string:** - All request parameters **except**: `file`, `cloud_name`, `resource_type`, `api_key` @@ -202,8 +322,8 @@ Signatures authenticate server-side upload requests. **The API secret must never 5. SHA-1 or SHA-256 hash the result **Timestamp rules:** -- Must be Unix timestamp in **seconds**, not milliseconds -- JavaScript: `Math.floor(Date.now() / 1000)` — `Date.now()` returns ms, divide by 1000 +- Must be a Unix timestamp in **seconds**, not milliseconds +- JavaScript: `Math.floor(Date.now() / 1000)`. `Date.now()` returns ms; divide by 1000 - Signature expires 1 hour after the timestamp **Example parameter string (before hashing):** @@ -211,27 +331,28 @@ Signatures authenticate server-side upload requests. **The API secret must never folder=uploads&public_id=my_image×tamp=1718100000YOUR_API_SECRET ``` -For complete algorithm with edge cases and examples, see [references/signed-uploads.md](references/signed-uploads.md). +For the complete algorithm with edge cases and examples, see [references/signed-uploads.md](references/signed-uploads.md). ## Security ### API secret - **Never** include `api_secret` in client-side code, browser requests, or mobile apps -- **Never** commit to version control (check `.env` files, config files) -- **Never** log it — check logging middleware and error handlers +- **Never** commit it to version control (check `.env` files, config files) +- **Never** log it. Check logging middleware and error handlers - If exposed: rotate immediately in Cloudinary Console → Settings → Security → Access Keys ### Unsigned preset exposure - The upload preset name is visible in browser network requests and source code -- Attackers can discover and reuse it to upload to your account (quota abuse) -- Unsigned uploads cannot overwrite existing assets — this limits damage +- Anyone can reuse it to upload to your account (quota abuse) +- Unsigned uploads cannot overwrite existing assets, which limits the damage +- Lock the preset down: `allowed_formats`, `max_bytes`, `asset_folder` or `folder`, and `moderation` for user-generated content - Use signed uploads for sensitive applications or when upload volume abuse is a concern ### Checklist before going live - API secret is server-side only - Upload preset is set to unsigned only if truly needed client-side - `allowed_formats` restricts file types to what your app expects -- `max_bytes` set in preset to prevent oversized uploads +- `max_bytes` set in the preset to prevent oversized uploads ## Async Uploads @@ -245,7 +366,7 @@ Set `async: true` to process uploads in the background. Useful for large files o **Python SDK:** `async` is a reserved keyword. Pass it as a dictionary key: ```python -# Wrong — syntax error +# Wrong: syntax error cloudinary.uploader.upload("file.jpg", async=True) # Correct @@ -254,18 +375,25 @@ cloudinary.uploader.upload("file.jpg", **{"async": True}) cloudinary.uploader.upload("file.jpg", notification_url="https://...", **{"async": True}) ``` -## Generate + Validate Checklist +## Self-Validation Checklist **After generating any upload configuration or code, verify all of the following before returning:** -1. ✅ **`resource_type` explicitly set** — not relying on default `image`; matches the actual file type -2. ✅ **Unsigned param list valid** — request only contains parameters from the unsigned whitelist; everything else is in the preset -3. ✅ **`public_id` has no whitespace** — leading/trailing spaces cause silent fallback to random ID -4. ✅ **Signature timestamp in seconds** — not milliseconds (`Math.floor(Date.now() / 1000)` in JS) -5. ✅ **API secret not in client code** — only present in server-side signature generation -6. ✅ **`notification_url` set when `async: true`** — without it, upload result is lost -7. ✅ **`overwrite: true` not expected in unsigned flow** — silently forced to `false` -8. ✅ **Large file (> 100 MB) → chunked upload** — plain upload will fail or time out +1. ✅ **Upload preset used** for any repeated upload pattern; request-time params limited to what genuinely varies per upload +2. ✅ **`resource_type` explicitly set**, matching the actual file type +3. ✅ **Naming strategy is deliberate**: deterministic `public_id`, `use_filename`, or an explicit decision to accept random IDs +4. ✅ **Folder parameter matches the folder mode**: `asset_folder` (+ `public_id_prefix` if needed) in dynamic mode, `folder` only in fixed mode +5. ✅ **Unsigned request only contains allow-listed parameters**, and nothing in the request expects to override a preset value that the preset wins +6. ✅ **No `f_auto` in `eager` or incoming `transformation`**; `f_auto` belongs on the delivery URL +7. ✅ **Eager transformations match the delivery URL string exactly**, or both use the same `t_` +8. ✅ **`eager_async: true` + `eager_notification_url`** for video or slow eager transformations +9. ✅ **`public_id` has no whitespace and no extension** (extension only for `raw`) +10. ✅ **Signature timestamp in seconds**, not milliseconds +11. ✅ **API secret not in client code**; only present in server-side signature generation +12. ✅ **`notification_url` set when `async: true`** +13. ✅ **`overwrite: true` not expected in an unsigned flow**; it is silently forced to `false` +14. ✅ **Large file (> 100 MB) → SDK large-upload method or chunked upload** +15. ✅ **`allowed_formats` and `max_bytes` set on any unsigned preset** ## Debugging Workflow @@ -280,16 +408,17 @@ Note the exact message before doing anything else. See [references/troubleshooti ### Step 2: Check `resource_type` Is the `resource_type` in the URL correct for the file being uploaded? -- URL contains `/image/upload/` but file is a video → change to `/video/upload/` -- Use `/auto/upload/` if file type varies +- URL contains `/image/upload/` but the file is a video → change to `/video/upload/` +- Use `/auto/upload/` if the file type varies ### Step 3: Check upload type (signed vs unsigned) - **Unsigned:** Is `upload_preset` in the request? Is it spelled correctly and set to unsigned in the Console? -- **Signed:** Is `api_key`, `timestamp`, and `signature` all present? Is timestamp in seconds? +- **Signed:** Are `api_key`, `timestamp`, and `signature` all present? Is the timestamp in seconds? -### Step 4: Check parameter validity -- **Unsigned:** Are any non-whitelisted parameters in the request? Remove them or move to preset. -- **All:** Is `public_id` free of whitespace? +### Step 4: Check parameter validity and precedence +- **Unsigned:** Are any non-allow-listed parameters in the request? Remove them or move them to the preset. +- **Unsigned:** Is a request parameter being "ignored"? Check whether the preset defines the same parameter; the preset wins for most of them. +- **All:** Is `public_id` free of whitespace? Is the folder parameter right for the folder mode? ### Step 5: Verify the signature (signed uploads only) 1. Did you exclude `file`, `cloud_name`, `resource_type`, `api_key` from the signature string? @@ -304,31 +433,66 @@ Open Cloudinary Console → Settings → Upload → [your preset] and verify: - Preset mode matches usage (signed vs unsigned) - `allowed_formats` isn't blocking the file type - `max_bytes` limit isn't exceeded +- Eager transformations don't contain `f_auto` -### Step 7: Check chunked upload (large files only) -- Is `Content-Range` format correct: `bytes -/`? +### Step 7: Eager asset not being used on delivery +- Does the delivery URL use the identical transformation string (same parameter order, same format component)? +- Delivering with `f_auto`? Pre-generate each format you expect `f_auto` to pick (for example `f_webp`, `f_avif`, `f_jpg`) as separate eager variants, and deliver without a file extension so the extension does not become part of the transformation string. +- Was `eager_async` used? The derived asset may not exist yet; check the `eager_notification_url` webhook. + +### Step 8: Check chunked upload (large files only) +- Is `Content-Range` in the form `bytes -/`? - Is the range inclusive on both ends? - Is `X-Unique-Upload-Id` the same value across all chunks? - Are all chunks except the last ≥ 5 MB? See [references/chunked-uploads.md](references/chunked-uploads.md) for arithmetic details. -### Step 8: Remote/fetch upload checks +### Step 9: Remote upload checks - Is the URL ≤ 255 characters? - Are special characters URL-encoded? - Is the URL publicly accessible without authentication? +## Common Mistakes & Best Practices + +**Avoid:** +- ❌ Passing `folder`, `tags`, `eager`, `allowed_formats` on every request → ✅ Put them in an upload preset and pass `upload_preset` +- ❌ `eager: c_fill,w_600,h_400,f_auto` → ✅ `eager: c_fill,h_400,w_600,f_webp|c_fill,h_400,w_600,f_jpg` (no `f_auto` at upload; explicit formats) +- ❌ Eager `c_fill,h_400,w_600`, delivery `c_fill,w_600,h_400` → ✅ Identical strings, or `t_card` in both +- ❌ `folder: products` in a dynamic-folder environment → ✅ `asset_folder: products` (+ `public_id_prefix: products` if the path belongs in the URL) +- ❌ `public_id: photo.jpg` → ✅ `public_id: photo` (extension only for `raw`) +- ❌ Expecting `overwrite: true` in an unsigned upload → ✅ Signed upload for overwrites +- ❌ Hand-rolling chunked upload in Node or Python → ✅ SDK `upload_large()` +- ❌ Incoming `transformation` to "optimize" originals → ✅ `eager` to warm derived assets; keep the original unless normalizing user-generated content + +**Always:** +- Recommend an upload preset for any repeated upload pattern +- Set `resource_type` explicitly +- Set `allowed_formats` and `max_bytes` on unsigned presets +- Use `eager_async: true` for video eager transformations +- Build delivery URLs with the cloudinary-transformations skill so they match eager output + ## Related Skills -- **cloudinary-next** and **cloudinary-react** — framework-specific upload wiring: API routes and server actions, upload widget components, and environment variable handling. -- **cloudinary-docs** — anything outside this skill's scope, looked up in the current Cloudinary documentation. +- **cloudinary-transformations**: build the delivery URLs that must match eager transformations, and define named transformations reused in `eager`. +- **cloudinary-next** and **cloudinary-react**: framework-specific upload wiring, including `CldUploadWidget`, signature API routes and server actions, and environment variable handling. +- **cloudinary-docs**: anything outside this skill's scope, looked up in the current Cloudinary documentation. ## Additional Resources -- [references/signed-uploads.md](references/signed-uploads.md) — Full signature algorithm with examples -- [references/chunked-uploads.md](references/chunked-uploads.md) — Content-Range arithmetic, chunk requirements -- [references/troubleshooting.md](references/troubleshooting.md) — Error messages and fixes -- [Upload API Reference](https://cloudinary.com/documentation/image_upload_api_reference.md) -- [Upload Images Documentation](https://cloudinary.com/documentation/upload_images.md) -- [Authentication Signatures](https://cloudinary.com/documentation/authentication_signatures.md) -- [Upload Presets](https://cloudinary.com/documentation/upload_presets.md) +### Skill References (Progressive Disclosure) +- [references/signed-uploads.md](references/signed-uploads.md) - Use when implementing signature generation by hand or debugging `Invalid signature` +- [references/chunked-uploads.md](references/chunked-uploads.md) - Use when implementing chunked upload over REST or debugging `Content-Range` errors +- [references/troubleshooting.md](references/troubleshooting.md) - Use when an upload returns an error or behaves unexpectedly + +### Core Cloudinary Documentation +- [Upload API Reference](https://cloudinary.com/documentation/image_upload_api_reference.md?install_source=skillspack&referrer=upload-skill) - All upload parameters, unsigned allow-list, response format +- [Upload Guide](https://cloudinary.com/documentation/upload_images.md?install_source=skillspack&referrer=upload-skill) - Upload methods, chunked upload, avoiding duplicates +- [Upload Parameters](https://cloudinary.com/documentation/upload_parameters.md?install_source=skillspack&referrer=upload-skill) - Naming, folders, replacing assets, audio uploads +- [Upload Presets](https://cloudinary.com/documentation/upload_presets.md?install_source=skillspack&referrer=upload-skill) - Creating presets, precedence rules, best practices +- [Eager and Incoming Transformations](https://cloudinary.com/documentation/eager_and_incoming_transformations.md?install_source=skillspack&referrer=upload-skill) +- [Folder Modes](https://cloudinary.com/documentation/folder_modes.md?install_source=skillspack&referrer=upload-skill) - Dynamic vs fixed folders, `asset_folder` vs `folder` +- [Upload Widget](https://cloudinary.com/documentation/upload_widget.md?install_source=skillspack&referrer=upload-skill) +- [Upload Widget Reference](https://cloudinary.com/documentation/upload_widget_reference.md?install_source=skillspack&referrer=upload-skill) - All widget options and events +- [Authentication Signatures](https://cloudinary.com/documentation/authentication_signatures.md?install_source=skillspack&referrer=upload-skill) +- [Client-Side Uploading](https://cloudinary.com/documentation/client_side_uploading.md?install_source=skillspack&referrer=upload-skill) diff --git a/skills/platform/cloudinary-upload/references/chunked-uploads.md b/skills/platform/cloudinary-upload/references/chunked-uploads.md index d7e296d..3b9d107 100644 --- a/skills/platform/cloudinary-upload/references/chunked-uploads.md +++ b/skills/platform/cloudinary-upload/references/chunked-uploads.md @@ -94,4 +94,4 @@ while offset < total_size: | Not handling `done: false` | Treating mid-upload as error | Only treat HTTP errors as failures | | Wrong `total` in Content-Range | `400 Invalid Content-Range` | Use actual file byte size, not estimated | -Official documentation: [Manual Chunked Upload](https://cloudinary.com/documentation/upload_images.md#manual_chunked_upload_rest) +Official documentation: [Manual Chunked Upload](https://cloudinary.com/documentation/upload_images.md?install_source=skillspack&referrer=upload-skill#manual_chunked_upload_rest) diff --git a/skills/platform/cloudinary-upload/references/signed-uploads.md b/skills/platform/cloudinary-upload/references/signed-uploads.md index deaa5dc..0419128 100644 --- a/skills/platform/cloudinary-upload/references/signed-uploads.md +++ b/skills/platform/cloudinary-upload/references/signed-uploads.md @@ -76,4 +76,4 @@ To debug a signature mismatch: 3. Verify sort order: compare your sorted keys to alphabetical 4. Verify timestamp value is ≤ 3600 seconds in the past -Official documentation: [Authentication Signatures](https://cloudinary.com/documentation/authentication_signatures.md) +Official documentation: [Authentication Signatures](https://cloudinary.com/documentation/authentication_signatures.md?install_source=skillspack&referrer=upload-skill) diff --git a/skills/platform/cloudinary-upload/references/troubleshooting.md b/skills/platform/cloudinary-upload/references/troubleshooting.md index afa19c9..fad3d7b 100644 --- a/skills/platform/cloudinary-upload/references/troubleshooting.md +++ b/skills/platform/cloudinary-upload/references/troubleshooting.md @@ -59,7 +59,12 @@ HTTP status is typically 400 (bad request) or 401 (auth failure). | Behavior | Root cause | Fix | |---|---|---| | Asset gets random public_id instead of expected | `public_id` has whitespace, or null/empty | Strip whitespace from `public_id` before upload | -| Extra parameters ignored in unsigned upload | Parameters not in unsigned whitelist | Move parameters to upload preset | +| Extra parameters ignored in unsigned upload | Parameters not in the unsigned allow-list | Move parameters to the upload preset | +| Allow-listed parameter (e.g. `folder`, `tags`) ignored in unsigned upload | Preset defines the same parameter; for unsigned uploads the preset wins (only `public_id` and `filename_override` come from the request; `context` and `metadata` merge) | Remove the parameter from the preset, or switch to a signed upload | +| Asset lands in the root folder despite slashes in `public_id` | Dynamic folder mode: `public_id` path does not set the folder | Set `asset_folder` (in the preset) | +| Folder path missing from delivery URL | Dynamic folder mode: `asset_folder` is not part of the `public_id` | Add `public_id_prefix` or `use_asset_folder_as_public_id_prefix: true` | +| Eager transformation exists but delivery still generates lazily | Delivery string differs from the eager string (parameter order, extension, `f_auto`) | Use identical strings or a shared `t_`; pre-generate explicit formats and deliver with `f_auto` and no extension | +| `f_auto` in `eager` or incoming `transformation` does nothing | No requesting browser at upload time | Use explicit formats (`f_webp`, `f_jpg`) in eager; keep `f_auto` on the delivery URL | | `overwrite: true` has no effect | Forced `false` for unsigned uploads | Switch to signed upload if overwrite is needed | | Format not converted as expected | File type is in `allowed_formats` — stored as-is | Remove file type from `allowed_formats` if conversion is needed | | Async upload result never arrives | `notification_url` not set | Add `notification_url` to request or preset |