> ## Documentation Index
> Fetch the complete documentation index at: https://cloudinary.com/documentation/llms.txt
> Use this file to discover all available pages before exploring further.

# Product asset workflows


This section outlines an example workflow for managing product assets. Use these recommendations as a guide and adjust the implementation as relevant to suit your company's needs.

Product asset workflows should be largely automated. Once you link Cloudinary assets with specific products, they can sync to the appropriate website components, already optimized and formatted for the ideal display. 

Below is a high-level recommended workflow for product assets:

![Product asset workflow diagram](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/v1743597059/docs/product_workflow.png "thumb: w_802,dpr_2, width:802, popup:true")

## Enrich and organize on upload

Upload assets directly to Cloudinary as your central source of truth for all images and videos via the [upload](image_upload_api_reference#upload) method, automated system triggers, or batch processing.  As part of the upload, apply metadata from your product database as well as automatically generated tags and moderation, normalization, etc. Cloudinary automatically generates public IDs and delivery URLs for the uploaded assets.

### Step 1: Set up folders and permissions
#### Creating folders

Start by organizing your product assets into a clear folder structure. This improves searchability and overall asset management, and is especially important because permissions and governance in the Media Library are enforced via folder management.

We recommend starting with a root folder like `products/`, and organizing by category.

Here's a sample folder structure for your product assets:

```
products/
  clothing/
    men/
    women/
  accessories/
    bags/
    jewelry/
  footwear/
    sneakers/
    boots/
```
**To create a folder:**

1. Click the **Folders** tab at the top of the Media Library to navigate to that page.
2. Click the **Create a Folder** icon at the top left corner. 
3. To create a subfolder, navigate to the parent folder first.

![Create a folder](https://cloudinary-res.cloudinary.com/image/upload/c_crop,w_400,h_145,g_south_west/f_auto/q_auto/bo_1px_solid_grey/docs/DAM/create_folder.png "thumb: w_300,dpr_2, width:300, popup:true")
#### Sharing folders with Media Library users

While developers on your team may have **Admin** user roles, you should assign **Media Library user** roles to content editors in your organization who work with assets within the Media Library. For more information on adding users and assigning roles, see [User and group management](dam_admin_users_groups). 

Media Library users won't be able to view any content in the Media Library until you share folders with them. Once you do that, they'll be able to access the assets within the folder and its subfolders at the permission level you assign.

**To share a folder:**

1. Select **Share** from the options drop-down next to the current folder path at the top of the Media Library 
  ![Folder path drop-down](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1739201555/docs/DAM/folder_toolbar_expanded_new.png "thumb: w_200,dpr_2, width:250, with_url:false, with_code:false, popup:true")
2. Select the users and/or user groups you want to share the folder with, as well as the permission level for each. 

For more information, see [Folder management](dam_folders_collections_sharing#folder_management).

### Step 2: Associate an asset with its product SKU
You might be able to retrieve essential metadata, such as SKU or unique product ID, and product information embedded within the asset files themselves (e.g., EXIF, XMP). Alternatively, you could pull this information from your PIM, database, or spreadsheet containing filenames, SKUs, and other relevant details.

Depending on your use case, you can associate the asset with its product SKU within Cloudinary using:

{table:class=no-borders overview}Method | What it does | When to use it | Implementation 
---|---|---|---
| **Naming conventions** | Embeds SKU into the asset's public ID to link products across platforms. | Use this method only when you always associate each asset with only one product SKU. | Find out how to retrieve the product SKU:  - **If the SKU is part of the original file name**, create an upload preset with `use_filename: true`. See [Step 6](#step_6). - **If you need to extract the SKU from another location**, set the `public_id` on upload using a variable representing the `SKU`. See [Step 7](#step_7).
| **Metadata** | Stores SKUs in metadata fields, tags, PIM, or databases for flexible retrieval. | Use this method when you expect at least in some cases to associate a single asset with multiple product SKUs, or when public IDs aren't practical for SKU management. You might want to store the SKU in a metadata field even if assets are already linked to products using naming conventions to make them easier to find using search or filters. | 1 - Create a **structured metadata** field for storing SKUs. See [Step 4](#step_4). 2 - Set values for the **SKU** metadata field on your assets during upload. See [Step 7](#step_7).

### Step 3: Set up normalization
Normalization standardizes assets during upload, ensuring they meet your specific requirements for dimensions, format, and quality. This guarantees consistency across all product assets stored in your DAM and simplifies management.

This step entails creating named transformations to normalize product assets. You'll apply them on upload using an upload preset in [Step 6](#step_6).

> **TIP**: If having normalized versions of product assets in your DAM isn't essential, then instead of storing the normalized version of your product assets in Cloudinary on upload, you can consider keeping the original as is on upload and generating normalized variations upon delivery as derived assets. To do this, instead of applying the named transformation as an incoming transformation of an upload preset, use it as an [eager](eager_and_incoming_transformations#eager_transformations) transformation. See [Step 6](#step_6).

You can configure named transformations either [programmatically](admin_api#transformations) or using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder). The steps below describe how to use the Transformation Builder UI, which may be more convenient in many cases.

Some common normalization actions include:

{table:class=no-borders overview}Normalization Action | What it Does | Considerations | Implementation  
---|---|---|---  
| **Background removal** | Removes image backgrounds using AI. | Works well for large-scale operations. For finer results, use the [Pexels](remove_the_background_image_editing_addon#banner) add-on or [FinalTouch](finaltouch). | Create a [named transformation](image_transformations#named_transformations) using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder). Apply the [Background Removal](transformation_reference#e_background_removal) action.**Note**: You first need to register for the Cloudinary AI Background Removal [add-on](https://console.cloudinary.com/app/settings/addons). |  
| **Drop shadow** | Adds realistic shadows to background-removed images. | Configures a virtual light source for natural shading. | Create a [named transformation](image_transformations#named_transformations) using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder). Apply the [Drop Shadow](transformation_reference#e_shadow) action.  |  
| **Cropping and resizing** | Adjusts images to a specific aspect ratio. | Use **[`g_auto`](transformation_reference#g_auto)** or **[`g_object`](transformation_reference#g_object)** to keep key elements in focus for all `fill` and `crop` resize methods.If preserving the full image, use **[`c_pad`](transformation_reference#c_pad)** or **[`e_gen_fill`](transformation_reference#e_gen_fill)** to expand the background naturally on product assets where you aren't removing the background. | Create a [named transformation](image_transformations#named_transformations) using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder). When cropping with **Fill** or **Crop** actions, select [Auto](transformation_reference#g_auto) or [Object](transformation_reference#g_object) from the **Focus on** drop-down to keep key elements in focus. When using the [Pad](transformation_reference#c_pad) resize action, select the [Generative Fill](transformation_reference#e_gen_fill) option from **Background Type** to expand the background naturally. |  
| **Trimming and removing sound** | Shortens video clips and removes audio as needed. | Helps optimize video length and mute unwanted audio. | Create a [named transformation](image_transformations#named_transformations) using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder). Apply the [Trim Video](transformation_reference#e_trim) action to cut video clips. Click **Quick Edit** and enter [`e_volume`](transformation_reference#e_volume) to remove audio. |  
| **Changing video orientation** | Auto-adjusts video orientation to keep the subject in focus. | Use **[`g_auto`](transformation_reference#g_auto)** to ensure proper alignment across different devices. | Create a [named transformation](image_transformations#named_transformations) using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder). Apply **Crop**, **Fill**, or **Pad** resize actions. Make sure to select [Auto](transformation_reference#g_auto) or [Object](transformation_reference#g_object) from the **Focus on** drop-down to keep key elements in focus. |  

> **NOTE**: Apply the named transformation you created in this step on upload using an upload preset. See [Step 6](#step_6) and [Step 7](#step_7).

#### Example: Resize and remove background

The transformation applied to the normalized image on the right removes the background, fits the subject within a 1920×1080 box without distortion, and then crops it to exactly 1920×1080 using automatic centering (`e_background_removal/c_crop,g_auto,h_1080,w_1920`). This results in a transparent image with consistent dimensions, suitable for use in standardized layouts.

  
    
      
    
    Original
  

  
    
      
    
    Normalized
  

#### How to set up this named transformation * In the Transformation Builder, add the **Background Removal** action, then add the **Auto Crop** action with the desired width and height.

* Alternatively, click **Quick Edit** and enter the `e_background_removal/c_crop,g_auto,h_1080,w_1920` transformation string. 

> **TIP**: The `background_removal` transformation must be first.

![Normalization Example](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1745767946/docs/normalization_ex_transformation_builder.png "thumb: dpr_2,w_800, width:800, with_code:false, with_url:false, popup:true")

For full instructions on setting up named transformations, see below.

#### Using the Transformation Builder

Configure named transformations using the [Transformation Builder](https://console.cloudinary.com/app/image/transformation_builder) UI following these instructions:

1. Open the Cloudinary Console, click the **Image** product, then click **New Transformation** to access the **Transformation Builder**. 
   > **TIP**:
>
> You can use sample assets or select your own in the **Transformation Builder** to preview how transformations look when applied. 
2. Select the transformations you want to apply from the **Select Action** area. You can also click **Quick Edit** to manually add transformation strings. See a complete list in the [Transformation URL API reference](transformation_reference). 

#### Video Tutorial: Creating named transformations
  
Watch a short video that shows how to create named transformations in the **Transformation Builder** for normalizing product assets.

> **TIP**:
>
> * Learn more about background removal and drop shadow effect in the [Remove Image Backgrounds and Add Realistic Shadows to Products in React](https://cloudinary.com/blog/remove-image-backgrounds-and-add-realistic-shadows-to-products-in-react) blog post.

> * In the Transformation Builder, you can override automatic crops by clicking the edit icon on the image and cropping manually.

### Step 4: Create metadata fields
Metadata can help system users, developers, or third-party stakeholders search for and retrieve assets for management or delivery. After you create metadata fields for your product environment, you can populate the fields by retrieving metadata from information within the uploaded files themselves or programmatically sync values between your assets and your product management platform.  You can create the fields programmatically or via the Cloudinary Console UI.

**To define metadata fields in the Console**:

1. Go to the [Structured Metadata](https://console.cloudinary.com/console/media_library/metadata_fields) page in the Console.
2. Configure metadata. Consider creating:
   * **SKU** field of type `text` with external ID `sku_id`
   * **Expiration** field of type `date` with external ID `expiration_date_id`
   * **Visibility** field of type `single select` with external ID `visibility_id` and list values **Public** and **Private** 
3. The following options are available when creating and managing fields:
  * **Field types**: Text, number, date, single-selection, and multiple-selection lists. 
  * **Mandatory fields**: Define required fields to ensure consistent metadata entry. (Make sure you set a realistic number of mandatory fields so your users don't get overwhelmed.)
  * **Default values**: Assign default values to mandatory fields to prevent upload failures.
  * **List values**: Specify predefined values for single- and multiple-selection fields.
  * **Metadata rules**: Set conditional rules (e.g., **State** field appears only if **Country** is **US**).![Create and manage structured metadata fields](https://cloudinary-res.cloudinary.com/image/upload/q_auto/f_auto/bo_1px_solid_grey/v1743540292/docs/smd_create_ecommerce.png "thumb: w_800,dpr_2, width:800, popup:true")For more information, see Structured metadata.

> **TIP**:
>
> You can also manage metadata fields programmatically using the [metadata_fields](admin_api#metadata_fields) method in the Admin API, although using the **Structured Metadata** page might be more convenient.

### Step 5: Register for add-ons
In order to enable some upload preset options, you first need to register for Cloudinary's [add-ons](https://console.cloudinary.com/app/settings/addons). You'll then activate the selected add-ons using upload preset options in [Step 6](#step_6).

Here are some of the add-ons you may want to apply:

{table:class=no-borders overview}Add-on | What it Does | Considerations | Implementation  
---|---|---|---  
| **Background removal** | Remove backgrounds quickly and accurately to create transparent images that showcase the main subject. |  Works well for large-scale operations. For finer results, use the [Pexels](remove_the_background_image_editing_addon#banner) add-on or [FinalTouch](finaltouch). | 1 - Register for the **Cloudinary AI Background Removal** add-on in the [Add-ons](https://console.cloudinary.com/app/settings/addons) page.  2 - Enable **Cloudinary AI Background Removal** in **Addons** page of a new upload preset.  |  
| **Google Auto Tagging** | Automatically tags images using Google Vision AI. | Works well for general **object recognition** and **categorization**. | 1 - Register for the **Google Auto Tagging** add-on.  2 - Enable **Google Auto Tagging** in **Addons** page of a new upload preset.  |  
| **Amazon Rekognition Auto Tagging** | Detects objects and assigns tags using Amazon Rekognition. | Useful for AI-powered **object detection and scene analysis**. | 1 - Register for the **Amazon Rekognition Auto Tagging** add-on.  2 - Enable **Rekognition Auto Tagging** in the **Addons** page of a new upload preset.  |  
| **Auto Moderation** | Flags inappropriate content. | Ideal for **content moderation** and **brand safety**. | 1 - Register for the **Rekognition AI Moderation** and **Rekognition AI Video Moderation** add-on.  2 - Enable **Rekognition AI Moderation** (for images) or **Rekognition AI Video Moderation** (for videos) in the **Addons** page of a new upload preset.  |  
| **Cloudinary AI Content Analysis** | Generates image captions and auto-tags assets. | Useful for **automatic alt text** and **SEO improvements**. | 1 - Register for the **Cloudinary AI Content Analysis** add-on.  2 - Enable **Add AI captioning to your image** for the **Cloudinary AI Content Analysis** add-on in the **AI media processing**  section in the **Addons** page of a new upload preset. See [Step 6](#step_6) for full upload preset configuration instructions. |  
| **Google Video Intelligence** | Labels objects and scenes in videos. | Ideal for **video content classification**. | 1 - Register for the **Google Video Intelligence** add-on.  2 - Enable **Google Automatic Video Tagging** in the **Addons** page of a new upload preset.  |  
| **Video Transcription and Translation** | Generates **video transcriptions** and translates them into multiple languages. | Enhances **accessibility**, **SEO**, and **multi-language support**. | 1 - Register for the **Video Transcription and Translation** add-on.  2 - Enable **Video Transcription and Translation** in the **Addons** page of a new upload preset. |  
| **Cloudinary AI Vision** | Allows you to ask questions or make requests about the content of an image. | Use Cloudinary AI Vision to automatically decide which crops to apply to images by asking questions such as "Is the clothing item floating without a model in the image?" or "Does the image have the top of the main subject (model or product) cropped?". | 1 - Register for the **Cloudinary AI Vision** add-on.  2 - Call the call the `ai_vision_tagging` method, as described in the [Cloudinary AI Vision](cloudinary_ai_vision_addon#tagging_mode) documentation.

### Step 6: Use an upload preset for normalization, auto-tagging, access control, alt-text generation, and optimization
An upload preset enables you to centrally define a set of upload options, ensuring consistency and efficiency when uploading assets. It applies the options you set up in the previous steps, as well as other best practices for metadata, access control, normalization, accessibility, and optimization.

**To create an upload preset to apply upload options:** 

* Navigate to the [Upload Presets](https://console.cloudinary.com/app/settings/upload/presets) tab of the Upload Console Settings page and click **Add Upload Preset**.

* Use the table below to apply recommended upload preset options, see how they align with best practices, and learn how to implement them.

{table:class=wide-lastcol-img no-borders overview}Option | How it Supports Best Practices | Implementation
---|---|---
| Naming and [folder location](#step_1) | Enforce naming conventions such as SKU association with the asset. Enforce governance within the organization and improve discoverability by uploading assets to meaningful folder locations. | In the **General** tab of the new upload preset:  - Enter the name of the **Asset folder** you want to upload to. - Select **Use the filename of the uploaded file as the public ID** to enforce naming conventions when the uploaded file contains the SKU. **Note:** If the SKU isn't part of the uploaded file's name and the SKU needs to be part of the public ID, assign the public ID directly in the upload call. For more information, see [Step 7](#step_7).![Upload preset - asset naming](https://cloudinary-res.cloudinary.com/image/upload/q_auto/f_auto/bo_1px_solid_grey/v1740940042/docs/upload_preset_product_general.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true")|
| [Normalize assets](#step_2) | Apply named transformations as incoming transformations to [normalize](ecommerce_best_practices#store_once_transform_dynamically) the assets you store in Cloudinary. | In the **Transform** tab of the new upload preset, enter the name of the named transformation preceded by `t_`:![Upload preset - named transformation](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1740917086/docs/upload_preset_named_transformation.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true")**Note:** Instead of saving the normalized version of the asset in Cloudinary, you can save the original and generate a derived asset modified for later use. Do this by entering the named transformation as an [Eager transformation](eager_and_incoming_transformations#eager_transformations), instead of [Incoming](eager_and_incoming_transformations#incoming_transformations).|
| **Improve image and video performance** | Prepare responsive images for various viewports and use auto-format and quality settings to make sure your website [loads quickly](ecommerce_best_practices#optimize_image_and_video_performance). | In the **Transform** tab of the new upload preset, add `f_auto/q_auto` as [Eager transformations](eager_and_incoming_transformations#eager_transformations) for various viewport sizes, e.g., `w_320`, `w_768`, `w_1280`, `w_1600`.![Upload preset - eager](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1740920187/docs/upload_preset_eager.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true") | 
| **Enhance searchability with auto-tagging** |  [Categorize](ecommerce_best_practices#enhance_searchability_with_metadata) assets accurately for faster search and retrieval while reducing manual effort. | In the **Addons** tab of the new upload preset, select the auto-tagging add-on(s). Make sure you're registered for those add-ons, as described in [Step 5](#step_5).![Upload preset - auto-tagging](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1740920148/docs/upload_preset_auto_tagging.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true") |  
| **Restrict access to assets with time based availability** | Ensures [sensitive assets](ecommerce_best_practices#manage_asset_rights_and_compliance) are only available when needed.  |  In the **Optimize and Deliver** tab of the new upload preset, set **Access control** to **Restricted**, with an optional date range for unrestricted availability.![Upload preset - access control](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1740920173/docs/uploal_preset_access_control.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true")
| **Auto-generate captions** | Use these captions as [alt text](ecommerce_best_practices#enhancing_accessibility_with_alt_text) to make images accessible to visually impaired users and improve SEO. | In the **Addons** tab of the new upload preset, select **Add Al captioning to your image** from the **Cloudinary Al Content Analysis** add-on. Make sure you're registered for the **Cloudinary AI Content Analysis** add-on, as described in [Step 5](#step_5).![Upload preset - auto-captioning](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1740920640/docs/upload_preset_captioning.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true") To add the caption to a previously created structured metadata field called **Caption** (whose external_id is `auto_caption`) and add the value *autocaption* to the assets' tags, add the following code in the **On success** script section of the **Advanced** tab of your upload preset:`current_asset.update({tags: ['autocaption'], metadata: {auto_caption: e.upload_info?.info?.detection?.captioning?.data?.caption}})`![Upload preset - auto caption eval](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1743597869/docs/auto_caption_eval_upload_preset.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true") After the upload, retrieve the structured metadata field value for an asset and assign it to its alt text when rendering it in your HTML code.|  
| **Generate video transcription and translations**| Use [video transcription and translation](ecommerce_best_practices#improving_video_accessibility_with_transcripts_and_subtitles) to make images and videos accessible to deaf and hard-of-hearing users and expand reach to multilingual audiences. | In the **Manage and Analyze** tab of the new upload preset, turn on **Auto transcription** and select the languages you want to provide. Make sure you're registered for the **Video Transcription and Translation** add-on , as described in [Step 5](#step_5).![Upload preset - transcription](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1740920227/docs/upload_preset_transcription.png "thumb: w_550,dpr_2, width:550, with_code:false, with_url:false, popup:true") |

* Remember to click **Save** at the top left corner.

> **TIP**:
>
> You can also manage upload presets programmatically using the [upload_presets](admin_api#upload_presets) method in the Admin API, although using the **Structured Metadata** page might be more convenient. You can configure upload presets with any of the parameters available for a regular [upload](image_upload_api_reference#upload) call.

### Step 7: Upload with an upload preset and metadata field values
Upload assets to Cloudinary with the upload preset applied, use:

* **The [upload](image_upload_api_reference#upload) method** of the Upload API for single asset or batch uploads.
* **The [migrate](cloudinary_cli#migrate) command** of the Cloudinary CLI for batch uploads. Use this method for initiating uploads automatically from another source.
* **The Media Library** for manually uploading one or multiple assets, or a folder structure.

> **TIP**:
>
> You can update metadata for assets that are already uploaded:

> * Update metadata programmatically using the [explicit](image_upload_api_reference#explicit) method.

> * Bulk set metadata in the Media Library by selecting the assets you want to update and selecting **Edit Structured Metadata** from the assets toolbar, or individually from the [Metadata](dam_manage_metadata#setting_custom_metadata_values) tab of the asset management drill-down page.

#### Example: Upload using the Upload method

Use the [upload](image_upload_api_reference#upload) method to upload an asset using the `product_images` preset. In this example, metadata values are set for the following structured metadata fields:

* **SKU** (`sku_id`)

* **Expiration date** (`expiration_date_id`, set to 2 months from today)

* **Visibility** (`visibility_id`)

Retrieved the SKU from your database or PIM using the `get_sku()` function. Assign the SKU value to the metadata field, and include the SKU in the public ID as a prefix, followed by a generated suffix.

```multi
|python
import cloudinary.uploader

# Retrieve values from your system
sku = get_sku()  # Custom function to get SKU from database or PIM
expiration_date = (datetime.now() + timedelta(days=60)).strftime('%Y-%m-%d')
visibility = 'public'

# Construct the public ID using SKU as a prefix
public_id_value = sku + "_" + generate_suffix()

# Upload the asset with metadata and a preset
upload_response = cloudinary.uploader.upload(
    'path_to_asset',
    upload_preset='product_images',
    public_id=public_id_value,
    metadata=f'expiration_date_id={expiration_date}|sku_id={sku}|visibility_id={visibility}'
)

|nodejs
const moment = require('moment');
const cloudinary = require('cloudinary').v2;

// Generate metadata values
const expirationDate = moment().add(2, 'months').format('YYYY-MM-DD');
const sku = getSku(); // Replace with function to get SKU from your system
const visibility = 'public';
const publicId = `${sku}_${generateSuffix()}`; // Replace with your suffix logic

cloudinary.uploader.upload('path_to_asset', {
  upload_preset: 'product_images',
  public_id: publicId,
  metadata: `expiration_date_id=${expirationDate}|sku_id=${sku}|visibility_id=${visibility}`
}, function(error, result) {
  if (error) {
    console.error('Error uploading asset:', error);
  } else {
    console.log('Upload response:', result);
  }
});

|ruby
require 'date'

# Generate metadata values
expiration_date = (Date.today + 60).to_s
sku = get_sku # Replace with function to get SKU from your system
visibility = 'public'
public_id = "#{sku}_#{generate_suffix}" # Replace with your suffix logic

upload_response = Cloudinary::Uploader.upload(
  'path_to_asset',
  upload_preset: 'product_images',
  public_id: public_id,
  metadata: "expiration_date_id=#{expiration_date}|sku_id=#{sku}|visibility_id=#{visibility}"
)

|php_2
$expiration_date = date('Y-m-d', strtotime('+60 days')); // 60 days from now
$sku = getSku(); // Replace with function to get SKU from your system
$visibility = 'public';
$public_id = $sku . '_' . generateSuffix(); // Replace with your suffix logic

try {
    $uploadResponse = $uploadApi->upload('path_to_your_image.jpg', [
        'upload_preset' => 'product_images',
        'public_id' => $public_id,
        'metadata' => "expiration_date_id={$expiration_date}|sku_id={$sku}|visibility_id={$visibility}"
    ]);

    echo "Image uploaded successfully: " . json_encode($uploadResponse) . "\n";

} catch (\Exception $e) {
    echo "Error uploading image: " . $e->getMessage() . "\n";
}
```

#### Example: Batch upload with the CLI

Batch upload using the [migrate](cloudinary_cli#migrate) command:

1. Create an upload mapping to transfer assets from your source domain `https://your-source-domain.com/media/uploads/` to Cloudinary’s `product_assets` folder
  
  ```
  cld admin create_upload_mapping "product_assets" template="https://your-source-domain.com/media/uploads/"
  ```

Run this command to migrate all the assets listed in the `url_file.txt` to the `product_assets` folder in your Cloudinary product environment, showing verbose output. The `product_assets` upload preset, which has the same name as the upload mapping, gets applied automatically:
  
  ```
  cld migrate -v -d ";" product_assets url_file.txt
  ```

You can update metadata programmatically using the [explicit](image_upload_api_reference#explicit) method.
  
```multi
|python
expiration_date = (datetime.now() + timedelta(days=60)).strftime('%Y-%m-%d')
sku = get_sku()  # Custom function to get SKU from database or PIM
visibility = 'public'

result = cloudinary.uploader.explicit("<public_id>", type = "upload",
metadata = 'sku_id=sku|visibility_id=visibility|expiration_date_id=expiration_date')

|nodejs
const expirationDate = moment().add(2, 'months').format('YYYY-MM-DD');
const sku = getSku(); // Replace with function to get SKU from your system
const visibility = 'public';

cloudinary.v2.uploader
.explicit("<public_id>", 
{ type: "upload",
    metadata: "sku_id=sku|visibility_id=visibility|expiration_date_id=expiration_date"})
.then(result=>console.log(result));

|ruby
expiration_date = (Date.today + 60).to_s
sku = get_sku # Replace with function to get SKU from your system
visibility = 'public'

result = Cloudinary::Upload.explicit("<public_id>", type: "upload",
  metadata: "sku_id=sku|visibility_id=visibility|expiration_date_id=expiration_date")

|php_2
$expiration_date = date('Y-m-d', strtotime('+60 days')); // 60 days from now
$sku = getSku(); // Replace with function to get SKU from your system
$visibility = 'public';

$result = $cloudinary->uploadApi()
  ->explicit("<public_id>", [
    "type" => "upload", 
    "metadata" => "sku=$sku|visibility=$visibility|expiration_date=$expiration_date"]);
```

#### Example: Upload using Media Library

Upload assets via the Media Library, applying upload preset options and adding metadata:
* **Default upload preset**: You can set up [Media Library Upload Preset Defaults](https://console.cloudinary.com/app/settings/c-fb59c6294cba357effd99e13a776a7/upload/defaults) for your images and videos. Select an upload preset that will apply to all uploads performed from the Media Library, whether the upload is done by dragging and dropping into the Media Library or via the Upload Widget by clicking the **Upload** button.

* **Upload preset selection**: For flexibility in selecting upload presets, you can click **Upload** to launch the Upload Widget, then select an Upload Preset in the **Advanced** section and select the appropriate preset from the **Upload Preset** drop-down.

![Upload Widget - select upload preset](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1739205001/docs/upload_widget_nonproduct_preset.png "thumb: w_500,dpr_2, width:500, popup:true")

* **Require metadata on upload**: In the [Upload](https://console.cloudinary.com/console/media_library/preferences/upload) page of the Preferences, select **Always** for the **Request metadata on upload**. When that setting is enabled, Cloudinary prompts filling in metadata field values before finalizing the upload.

![Metadata on upload](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1643036445/docs/DAM/upload_new_flow.png "thumb: w_300,dpr_2, width:300, with_code:false, with_url:false, popup:true")

> **NOTE**:
>
> Depending on our setup, upload presets might not be available in the Upload widget, and the **Request metadata on upload** setting may not be available. For more information, [contact support](https://support.cloudinary.com/hc/en-us/requests/new).

## Sync assets with your product data

After uploading your assets to Cloudinary, ensure they're linked to the other elements within your tech stack. Use product-identifying details (like SKU or product ID) in the asset's public ID or metadata for seamless integration.

### Sync assets using a PIM integration

For businesses utilizing a PIM (Product Information Management) system, product information often exists in the PIM before adding assets to Cloudinary. Cloudinary serves as the central source of truth, allowing PIM systems to pull asset data, such as public IDs and delivery URLs directly from Cloudinary.

Key benefits of PIM integration:

* **Automatic syncing**: Cloudinary delivery URLs or public IDs are stored in the PIM, enabling seamless product publishing with Cloudinary assets.

* **Consistent metadata**: Cloudinary retrieves and syncs product metadata from the PIM to ensure consistency.

* **Bi-directional updates**: Changes in the PIM automatically update in Cloudinary, and vice versa, ensuring the latest assets are always used.

[Akeneo](mediaflows_block_reference#akeneo) and [Syndigo](syndigo_partner_built_integration) are examples of Cloudinary-integrated PIMs. See [Integrations](integrations) for a complete list.

### Sync assets without a PIM integration

If you don’t use a PIM and instead manage product information through a database or spreadsheet, you can create a custom connector between Cloudinary and your system.

**Essential components of a custom integration**

{table:class=no-borders overview}Action | Purpose | Example
---|---|---
| Set up [webhook notifications](notifications) | Receive real-time updates when API calls and Media Library actions complete. | Configure a webhook to notify your system whenever a new asset gets added. |
| Save public IDs/URLs | Store asset identifiers in your database or spreadsheet for future reference. | Store Cloudinary public IDs alongside product SKUs in your database. |
| Use [MediaFlows](mediaflows) for automation | Automate metadata updates and asset syncing between Cloudinary and external systems. | Set up an automation that updates asset tags in Cloudinary based on changes detected in your product database. |
| Daily batch sync | Check for updates and synchronize metadata between Cloudinary and your system. | Use [MediaFlows](mediaflows) to run a scheduled job every night that syncs updated asset metadata. |
| Bi-directional sync | Export asset public IDs and URLs from Cloudinary and import product information into Cloudinary. | Automatically sync product descriptions from your database to Cloudinary metadata fields. |
| [Read-only fields](admin_api#metadata_field_structure) | Prevent Cloudinary metadata fields from being overwritten by external sources using the `restrictions` parameter within the metadata field structure. | Lock the Cloudinary **SKU** metadata field, as it receives a value by importing metadata from your database. |

#### Additional considerations for integration

* Make sure your connector is robust and handles all possible scenarios, such as asset deletion. Sync deletions from Cloudinary to your PIM or database.

* Here's an example of a personalized flow that can help you manage metadata sync between Cloudinary and your system. 
    * Use a structured metadata field called **Status**.
    * Set a value for the **Status** field dynamically:
      * **If all of the asset's required metadata is import successfully** from your PIM or database to Cloudinary, set the **Status** field to **Metadata complete**.
      * **If any required fields are missing**, set **Status** to **Requires data**.
    * Create a [saved search](dam_advanced_search#saved_searches) in Cloudinary to quickly find assets whose **Status** field's value is **Requires data** to manually complete the missing fields more easily.

#### Sample app: Syncing without a PIM integration

This [PHP sample app](php_sample_projects) adds products to a database and displays them in a product catalog with normalized images and videos. Videos undergo asynchronous moderation and are processed via webhook upon approval.

While this example provides a strong foundation, a real-world implementation would require modifying the process to retrieve assets and metadata programmatically instead of relying on user input, as well as enabling bulk uploads for efficiency.

> **TIP**: :title=View the code

You can find the code for this sample project in [GitHub](https://github.com/cloudinary-devs/php-product-catalog).

## Customize and optimize on delivery

Whether you've stored the original asset in your product environment or a normalized version, when delivering product assets to your website, you may need to adjust, customize, and optimize them. For example, you might need to adjust the size or aspect ratio based on whether the asset appears on a Product Detail Page (PDP) or Product Listing Page (PLP). Or, you might want to overlay a sale badge or other promotional elements.

Create named transformations programmatically or using the Transformation Builder. We recommend using named transformations over direct transformations as they shorten URLs for better SEO, are easy to reuse, and are simpler to apply when given meaningful names.

Once you've created a named transformation, you can:

* **Apply it directly to transform an asset**. For example, the following example applies the `clothing_sale` named transformation to the `woman-in-shirt` image, applying the sales tag:![Apply named transformation](https://res.cloudinary.com/demo/image/upload/t_clothing_sale/docs/sweaters/woman-in-shirt.jpg "thumb: w_250,dpr_2, width:250, with_code:true, with_url:true, with_image:true, popup:true")

```nodejs
cloudinary.image("docs/sweaters/woman-in-shirt.jpg", {transformation: ["clothing_sale"]})
```

```react
new CloudinaryImage("docs/sweaters/woman-in-shirt.jpg").namedTransformation(
  name("clothing_sale")
);
```

```vue
new CloudinaryImage("docs/sweaters/woman-in-shirt.jpg").namedTransformation(
  name("clothing_sale")
);
```

```angular
new CloudinaryImage("docs/sweaters/woman-in-shirt.jpg").namedTransformation(
  name("clothing_sale")
);
```

```js
new CloudinaryImage("docs/sweaters/woman-in-shirt.jpg").namedTransformation(
  name("clothing_sale")
);
```

```python
CloudinaryImage("docs/sweaters/woman-in-shirt.jpg").image(transformation=["clothing_sale"])
```

```php
(new ImageTag('docs/sweaters/woman-in-shirt.jpg'))
	->namedTransformation(NamedTransformation::name("clothing_sale"));
```

```java
cloudinary.url().transformation(new Transformation().named("clothing_sale")).imageTag("docs/sweaters/woman-in-shirt.jpg");
```

```ruby
cl_image_tag("docs/sweaters/woman-in-shirt.jpg", transformation: ["clothing_sale"])
```

```csharp
cloudinary.Api.UrlImgUp.Transform(new Transformation().Named("clothing_sale")).BuildImageTag("docs/sweaters/woman-in-shirt.jpg")
```

```dart
cloudinary.image('docs/sweaters/woman-in-shirt.jpg').transformation(Transformation()
	.namedTransformation(NamedTransformation.name("clothing_sale")));
```

```swift
imageView.cldSetImage(cloudinary.createUrl().setTransformation(CLDTransformation().setNamed("clothing_sale")).generate("docs/sweaters/woman-in-shirt.jpg")!, cloudinary: cloudinary)
```

```android
MediaManager.get().url().transformation(new Transformation().named("clothing_sale")).generate("docs/sweaters/woman-in-shirt.jpg");
```

```flutter
cloudinary.image('docs/sweaters/woman-in-shirt.jpg').transformation(Transformation()
	.namedTransformation(NamedTransformation.name("clothing_sale")));
```

```kotlin
cloudinary.image {
	publicId("docs/sweaters/woman-in-shirt.jpg")
	 namedTransformation(NamedTransformation.name("clothing_sale")) 
}.generate()
```

```jquery
$.cloudinary.image("docs/sweaters/woman-in-shirt.jpg", {transformation: ["clothing_sale"]})
```

```react_native
new CloudinaryImage("docs/sweaters/woman-in-shirt.jpg").namedTransformation(
  name("clothing_sale")
);
```

* **Associate it with frontend components.** Matching assets to product SKUs automatically places images and videos in the right spot on your website. Linking named transformations to frontend components ensures each asset gets processed to fit design requirements.
  
> **TIP**:
>
> * If a transformation is short and not going to be reused, you can apply it directly to the asset instead of creating a named transformation.

> * You can use variables in the transformation, whether you apply them directly or as named transformations. For example, if you want to localize text overlays or switch out [generative AI](ai_in_action) prompts for specifying background requests or objects for removal. For more information, see the [Transformation reference](transformation_reference) and [User-defined variables and arithmetic image transformations](user_defined_variables).

For more information about creating named transformations to customize assets on delivery, see [Customize and optimize on delivery](ecommerce_optimize_customize).

## Deliver product assets to your e-commerce website

Once you've organized assets in Cloudinary and linked them to your products, delivering them to your e-commerce website becomes an automated process. Whether using a pre-built integration or a custom implementation, you can use Cloudinary to dynamically optimize and serve product images and videos based on product data and display requirements.

{table:class=no-borders overview}Method | What it does | Best for
---|---|---
| [Using an integrated front end](#using_an_integrated_front_end) | Cloudinary integrates directly with certain e-commerce platforms and headless CMSs, allowing automatic asset retrieval and display. Integrations provide a built-in way to apply optimization and transformations on delivery, using the named transformations you set up. | E-commerce stores built on platforms like Shopify, Salesforce Commerce Cloud, or commercetools. For a full listing, see [Integrations](integrations). |
| [Using a non-integrated front end](#using_a_standalone_front_end) | Developers can programmatically fetch assets using Cloudinary’s APIs and SDKs, applying real-time transformations for optimization using named transformations. | Custom-built websites or apps that don’t use a Cloudinary integration—for example, static sites, SPAs, or composable storefronts using manual asset delivery. |

In both cases, you can use the [Product Gallery Widget](#enhancing_product_asset_display_with_the_product_gallery_widget) to deliver product assets dynamically on Product Detail Pages (PDPs). The Product Gallery Widget provides an interactive viewer that enhances product display with automatic image retrieval, video support, and 360° spin sets.

### Using an integrated front end

If you’re using an e-commerce platform or a composable, headless front end with built-in Cloudinary integration, follow the platform’s configuration and setup instructions to display your assets efficiently.

The examples below highlight a few popular integration options, but Cloudinary supports a growing number of platforms.
To explore all available integrations, see the [Integrations](integrations) page.

* **E-commerce platform integration examples** 
  * [Shopify](mediaflows_block_reference#shopify): Utilize Cloudinary’s MediaFlows integration for seamless product asset delivery and rendering on Shopify stores, ensuring assets display as expected.
  * [Salesforce Commerce Cloud](salesforce_integration): Automate asset delivery using Salesforce Commerce Cloud Cartridges, ensuring efficient Cloudinary asset handling within your storefront.

* **Composable storefront integration examples**
  * [commercetools](commercetools_integration): Dynamically manage product assets for headless e-commerce experiences, providing flexibility in asset delivery.
  * [Salesforce Commerce Cloud Headless Cartridge](sfcc_b2c_commerce_cartridge_headless): Enable Cloudinary image and video management through Salesforce’s headless commerce for seamless API-driven asset delivery.

#### Example walkthrough
  
This fully functional [demo](https://cloudinary-commercetools-demo.netlify.app/) below showcases the commercetools extension, offering insights into setting up a storefront and displaying product assets effectively. 

PLP withresponsive images

> **TIP**: :title=View the code

You can find the code for this sample project in [GitHub](https://github.com/cloudinary/cloudinary_commercetools/).

### Using a standalone front end

If your platforms don't include a ready-made Cloudinary integration, you'll need to implement your own solution to render your Cloudinary assets on your front-end. 

Use Cloudinary's [SDKs](cloudinary_sdks) to seamlessly integrate assets (and optionally the Product Gallery Widget) into headless front-end applications (such as React, React Native, Android, etc.). Dynamically assign assets to website components using product-match workflows. 

#### Step 1: Schedule publishing 

Automatically initiate the process of publishing assets using [MediaFlows](mediaflows) or cron jobs, ensuring your assets are always up-to-date on your website without manual intervention.

Here's an example [MediaFlow](mediaflows) that sends an HTTPS request to update your front-end with any new or updated Cloudinary assets any on a schedule, such as once a day:

![MediaFlow - automated publishing](https://cloudinary-res.cloudinary.com/image/upload/f_auto/q_auto/bo_1px_solid_grey/v1739982395/docs/ecommerce_auto_products_example.png "thumb: dpr_2,w_800, width:800, with_code:false, with_url:false, popup:true")

#### Step 2: Optimize and transform assets according to website location

Use [named transformations](image_transformations#named_transformations) to dynamically modify product images based on their display location (e.g., PDPs, PLPs) while ensuring optimal format and quality. Once created, a named transformation can be applied repeatedly on delivery without additional setup.

Here's an example of programmatically creating and applying a named transformation (`pdp_product_images`) that applies cropping, resizing, format conversion, and quality adjustments: 

```multi
|ruby
require 'cloudinary'

Cloudinary::Api.create_transformation('pdp_product_images',
  transformation: [
    { width: 500, height: 500, crop: "fill" },
    { fetch_format: "auto" },
    { quality: "auto" }
  ]
)

image_url = Cloudinary::Utils.cloudinary_url('product_sku_123', transformation: 'pdp_product_images')
puts image_url

|php_2
use Cloudinary\Api\Admin\AdminApi;
use Cloudinary\Utils;

$api = new AdminApi();
$api->createTransformation([
  "name" => "pdp_product_images",
  "transformation" => [
    ["width" => 500, "height" => 500, "crop" => "fill"],
    ["fetch_format" => "auto"],
    ["quality" => "auto"]
  ]
]);

$imageUrl = Utils::cloudinary_url("product_sku_123", ["transformation" => "pdp_product_images"]);
echo $imageUrl;

|python
import cloudinary.api
import cloudinary.utils

cloudinary.api.create_transformation("pdp_product_images", transformation=[
    {"width": 500, "height": 500, "crop": "fill"},
    {"fetch_format": "auto"},
    {"quality": "auto"}
])

image_url = cloudinary.utils.cloudinary_url("product_sku_123", transformation="pdp_product_images")[0]
print(image_url)

|nodejs
const cloudinary = require('cloudinary').v2;

cloudinary.api.create_transformation('pdp_product_images', {
  transformation: [
    { width: 500, height: 500, crop: 'fill' },
    { fetch_format: 'auto' },
    { quality: 'auto' }
  ]
});

const imageUrl = cloudinary.url('product_sku_123', { transformation: 'pdp_product_images' });
console.log(imageUrl);

|java
import com.cloudinary.Cloudinary;
import com.cloudinary.utils.ObjectUtils;

public class CloudinaryExample {
    public static void main(String[] args) {
        Cloudinary cloudinary = new Cloudinary();

        try {
            cloudinary.api().createTransformation("pdp_product_images",
                ObjectUtils.asMap(
                    "transformation", new Object[]{
                        ObjectUtils.asMap("width", 500, "height", 500, "crop", "fill"),
                        ObjectUtils.asMap("fetch_format", "auto"),
                        ObjectUtils.asMap("quality", "auto")
                    }
                ));

            String imageUrl = cloudinary.url().transformation("pdp_product_images").generate("product_sku_123");
            System.out.println(imageUrl);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

|csharp
using CloudinaryDotNet.Actions;
using CloudinaryDotNet;

public class CloudinaryUtils {
    public static void CreateTransformation(Cloudinary cloudinary) {
        cloudinary.CreateTransform(new CreateTransformParams
        {
            Name = "pdp_product_images",
            Transformation = new Transformation()
                .Width(500).Height(500).Crop("fill")
                .FetchFormat("auto")
                .Quality("auto")
        });
    }

    public static string GetTransformedImageUrl(Cloudinary cloudinary, string publicId) {
        return cloudinary.Api.UrlImgUp.Transform(new Transformation().Named("pdp_product_images")).BuildUrl(publicId);
    }
}

Cloudinary cloudinary = new Cloudinary();
CloudinaryUtils.CreateTransformation(cloudinary);
string imageUrl = CloudinaryUtils.GetTransformedImageUrl(cloudinary, "product_sku_123");
Console.WriteLine(imageUrl);

|go
import (
	"context"
	"fmt"
	"log"

	"github.com/cloudinary/cloudinary-go/v2/api/admin"
)

func createTransformation(cld *cloudinary.Cloudinary) error {
	ctx := context.Background()
	_, err := cld.Admin.CreateTransformation(ctx, admin.CreateTransformationParams{
		Name: "pdp_product_images",
		Transformation: "w_500,h_500,c_fill/f_auto/q_auto",
	})
	return err
}

func getTransformedImageUrl(cld *cloudinary.Cloudinary, publicId string) string {
	return cld.Url(publicId).Transformation("pdp_product_images").String()
}

err := createTransformation(cld)
if err != nil {
	log.Println(err)
}

imageUrl := getTransformedImageUrl(cld, "product_sku_123")
fmt.Println(imageUrl)
```

> **TIP**: You can use the Transformation Builder to create named transformations to use on delivery, just like the ones you created for normalization on upload. For more information, see [Step 3](#step_3) of the upload process.

#### Step 3: Search, retrieve, and render product assets

To display the correct assets on your website, retrieve each asset by its SKU (or another product identifier). You can store the SKU as part of the public ID or within the asset’s structured metadata, as described above in [put the section name/link here]. This approach ensures each asset matches the correct product and display location, such as PDPs or PLPs.

The following example creates a delivery URL for an asset using the SKU stored in the metadata field. It applies the `pdp_product_images` named transformation to optimize the image for a PDP:

```multi
|ruby
require 'cloudinary'

def get_public_id_by_sku(sku)
  result = Cloudinary::Search.expression("metadata.sku=#{sku}").execute
  if result["resources"].any?
    public_id = result["resources"][0]["public_id"]
    image_url = Cloudinary::Utils.cloudinary_url(public_id, transformation: "pdp_product_images")
    return image_url
  end
  nil
end

image_url = get_public_id_by_sku("product_sku_123")

|php_2
use Cloudinary\Api\Search\SearchApi;
use Cloudinary\Utils;

function getPublicIdBySKU($sku) {
    $api = new SearchApi();
    $result = $api->expression("metadata.sku=$sku")->execute();
    if (count($result['resources']) > 0) {
        $publicId = $result['resources'][0]['public_id'];
        return Utils::cloudinary_url($publicId, ['transformation' => 'pdp_product_images']);
    }
    return null;
}

$imageUrl = getPublicIdBySKU("product_sku_123");

|python
import cloudinary.search
import cloudinary.utils

def get_public_id_by_sku(sku):
    result = cloudinary.search.Search().expression(f"metadata.sku={sku}").execute()
    if result["resources"]:
        public_id = result["resources"][0]["public_id"]
        return cloudinary.utils.cloudinary_url(public_id, transformation="pdp_product_images")[0]
    return None

image_url = get_public_id_by_sku("product_sku_123")

|nodejs
const cloudinary = require('cloudinary').v2;

async function getPublicIdBySKU(sku) {
  try {
    const result = await cloudinary.search.expression(`metadata.sku=${sku}`).execute();
    if (result.resources.length > 0) {
      const publicId = result.resources[0].public_id;
      return cloudinary.url(publicId, { transformation: 'pdp_product_images' });
    }
    return null;
  } catch (error) {
    console.error(error);
    return null;
  }
}

const imageUrl = await getPublicIdBySKU('product_sku_123');

|java
import com.cloudinary.Cloudinary;
import com.cloudinary.api.ApiResponse;
import java.util.Map;

public class CloudinaryUtils {
    public static String getPublicIdBySKU(Cloudinary cloudinary, String sku) {
        try {
            ApiResponse result = cloudinary.search()
                .expression("metadata.sku=" + sku)
                .execute();
            if (!((Map<?, ?>) result.get("resources")).isEmpty()) {
                String publicId = ((Map<?, ?>) ((Map<?, ?>) result.get("resources")).get(0)).get("public_id").toString();
                return cloudinary.url().transformation("pdp_product_images").generate(publicId);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

String imageUrl = CloudinaryUtils.getPublicIdBySKU(cloudinary, "product_sku_123");

|csharp
using CloudinaryDotNet.Actions;
using CloudinaryDotNet;

public class CloudinaryUtils {
    public static string GetPublicIdBySKU(Cloudinary cloudinary, string sku) {
        var searchParams = new SearchParams().Expression($"metadata.sku={sku}");
        var result = cloudinary.Search(searchParams);
        if (result.Resources.Length > 0) {
            return cloudinary.Api.UrlImgUp.Transform(new Transformation().Named("pdp_product_images"))
                .BuildUrl(result.Resources[0].PublicId);
        }
        return null;
    }
}

string imageUrl = CloudinaryUtils.GetPublicIdBySKU(cloudinary, "product_sku_123");

|go
import (
	"context"
	"fmt"
	"log"

	"github.com/cloudinary/cloudinary-go/v2/api/search"
)

func getPublicIdBySKU(cld *cloudinary.Cloudinary, sku string) (string, error) {
	ctx := context.Background()
	result, err := cld.Search.Search(&search.Params{
		Expression: fmt.Sprintf("metadata.sku=%s", sku),
	}).Execute(ctx)
	if err != nil {
		return "", err
	}
	if len(result.Resources) > 0 {
		publicId := result.Resources[0].PublicId
		imageUrl := cld.Url(publicId).Transformation("pdp_product_images").String()
		return imageUrl, nil
	}
	return "", nil
}

imageUrl, err := getPublicIdBySKU(cld, "product_sku_123")
if err != nil {
	log.Println(err)
}
```

### Enhancing product asset display with the Product Gallery Widget

Cloudinary’s Product Gallery Widget is a ready-to-use, interactive viewer that simplifies product asset presentation. It requires only JavaScript, making it easy to integrate into any web framework while eliminating the need for custom gallery development.

**Benefits**:

* **Seamless integration**: Embed a responsive, media-rich product gallery.
* **Automatic asset population**: Retrieves product images and videos based on tags.
* **Customizable layouts**: Adjust styles, transitions, and navigation.
* **360° spin sets**: Enable interactive product exploration with rotational image sets.

{table:class=no-borders overview}Factor | Considerations
---|---
Page Performance | - **Asset Optimization**: Apply Cloudinary’s transformations, such as `f_auto` and `q_auto`, to ensure images and videos are delivered in the most efficient formats and quality for each device and browser. This reduces file sizes and improves load times.  - **Widget Impact**: While the Product Gallery Widget offers valuable interactivity and enhances the product display, it introduces additional resources that may affect page load times. Evaluate the benefits of the widget's features against the potential impact on your site's performance.
Customization | Modify layouts, navigation, and transitions to align with your design requirements and provide a cohesive user experience.

**Implementation:**

* **For integrated front-ends**, check your integration's documentation for specific instructions on embedding the Product Gallery Widget.

* **For custom front-ends**, include the remote JavaScript file and initialize the widget with:

```js
<script src="https://product-gallery.cloudinary.com/latest/all.js" type="text/javascript">
</script>  

<div id="my-gallery" style="max-width:80%;margin:auto">
</div>

<script type="text/javascript">  
const myGallery= cloudinary.galleryWidget({
  container: "#my-gallery",
  cloudName: "demo",
  mediaAssets: [
    {tag: "electric_car_product_gallery_demo"}, // by default mediaType: "image"
    {tag: "electric_car_product_gallery_demo", mediaType: "video"}, 
    {tag: "electric_car_360_product_gallery_demo", mediaType: "spin"}
  ]
});

myGallery.render();
</script>
```
> **INFO**: For business-critical apps, you're advised to specify the version of the Product Gallery JavaScript file and not use `latest`, particularly if your implementation includes customizations.

See the [Product Gallery changelog](product_gallery_changelog) for available versions and [register for notifications](product_gallery_changelog#register_for_notifications) to stay informed of new releases. Only update the version numbers in your code after testing new releases.
> **TIP**:
>
> For framework-specific implementations, explore:

> * [React Product Gallery Explorer](https://stackblitz.com/edit/cloudinary-product-gallery-react) | [GitHub repo](https://github.com/cloudinary-devs/cloudinary-product-gallery-react)

> * [Angular Product Gallery Sandbox](https://codesandbox.io/s/product-gallery-angular-tf717l)

> * [Vue Product Gallery Sandbox](https://codesandbox.io/s/product-gallery-vue-tjdz7x)

> **See more**:
>
> For more information, see [Product Gallery](product_gallery#banner).

## Other delivery channels

Besides delivering non-product images on your website, you can also deliver to the following channels:

* **Social media**: Optimize images for different platform size and format requirements.

* **Third-party stakeholders**: Ensure compliance with background color or file format constraints and protect your assets by watermarking.

* **Email campaigns**: Maintain quality within file size and resolution limitations.

* **Market places**: Adhere to specific format, background, metadata, and naming rules.

For more information, see [Multi-channed content delivery](ecommerce_workflows_delivery_channels).

  

