Technical integration - Media in the grid

The media rules configured in the dashboard can be displayed on your website or app using the Discovery Product and Category search API.

Handle media in the API response

This step requires a developer to integrate and handle the API response of the Product and Category search API. This includes four important steps:

  1. Pass the feature parameter in the API request to get media.
  2. Identify the objects containing media between the product results. 
  3. Set the appropriate display (for static types like images) or rendering conditions (for HTML or text assets) based on the media type.
  4. Create styling for different assets to fit with your product grid styles.

Pass the feature parameter

The content_injection query parameter in the API request controls the media in the grid feature. It can take boolean values of true or false.

If true, all the active media rules are applied to the search request. The configured assets are returned in the API response at the specified slot positions, along with other search results.

Identify media in the response

Example response

Below is an example of the Search API response with content_injection=true (here, the first slot contains an image media asset):

{
  "response": {
    "numFound": 2,
    "start": 0,
    "docs": [
      {
        "pid": "16624",
        "content": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZVJYCtcLtMv4lgo9YEUhb3HViPS9aHqjxfA&s",
        "contentType": "image_url",
        "docType": "CONTENT",
        "metaInfo": ""
      },
      {
        "pid": "91800",
        "description": "The Kennedy Modular Sectional Corner Chair is an ideal choice for anyone looking to customize their living room seating arrangement.",
        "thumb_image": "https://pacific-demo-data.bloomreach.cloud/home/images/gen/webp/91800_0_image.webp",
        "inStock": "true",
        "onSale": "false",
        "variants": [
          "var1"
        ],
        "docType": "PRODUCT",
        "content": "",
        "contentType": "",
        "metaInfo": "",
        "sale_price": 799.99,
        "score": 99.44386,
        "brand": "Pacific Style",
        "price": 799.99,
        "url": "https://pacifichome.bloomreach.com/home/products/91800",
        "title": "Kennedy Modular Sectional Corner Chair",
        "sale_price_range": [
          799.99,
          799.99
        ],
        "price_range": [
          799.99,
          799.99
        ]
      }
    ]
  },
  "facet_counts": {},
  "category_map": {},
  "stats": {},
  "did_you_mean": [],
  "metadata": {}
}

In the above sample response, note the following fields in the first item of the docs array: 

  • docType: This field helps distinguish between PRODUCT and CONTENT doc types in the response.
  • contentType: For slots containing media, this field mentions the type of asset (image_url, html, text).
  • content: This field contains the raw content data of the asset, which can be rendered based on your needs. It is the same value as you see through the Show API response button in the dashboard.
  • metaInfo: The metadata that is added along with the asset through the dashboard media rule is available in this field.

The data of the content and metaInfo fields is also accessible in the dashboard:

Set display and rendering conditions

You can use the media information from the API response to display and render the assets as required.

Given below is an example of the logic and components to use to display product cards along with content cards.

Rendering logic

if (item.docType === 'content') {
  return renderContentContainer({
    content: item.content,
    type: item.contentType,
    pid: key,
  });
} else {
  return renderProduct(item as BrProduct);
}
{docs.map((item) =>
  item.docType === "content"
    ? <ContentCard key={item.pid} content={item} />
    : <ProductCard key={item.pid} product={item} />
)}
<div v-for="item in docs" :key="item.pid">
  <ProductCard v-if="item.docType === 'product'" :product="item" />
  <ContentCard v-else :content="item" />
</div>
<div *ngFor="let item of docs">
  <app-product-card *ngIf="item.docType === 'product'" [product]="item"></app-product-card>
  <app-content-card *ngIf="item.docType === 'content'" [content]="item"></app-content-card>
</div>
export default function Home({ docs }: { docs: ApiDoc[] }) {
  return (
    <div className="grid grid-cols-2 gap-4">
      {docs.map((item) =>
        item.docType === "content"
          ? <ContentCard key={item.pid} content={item} />
          : <ProductCard key={item.pid} product={item} />
      )}
    </div>
  );
}

export const getServerSideProps = async () => {
  const docs = [
    { docType: "product", pid: "123", title: "Running Shoes" },
    { docType: "content", pid: "banner-1", contentType: "html", content: "<h2>Big Sale!</h2>" }
  ];
  return { props: { docs } };
};
export default function Home({ docs }) {
  return (
    <div className="grid grid-cols-2 gap-4">
      {docs.map((item) =>
        item.docType === "content"
          ? <ContentCard key={item.pid} content={item} />
          : <ProductCard key={item.pid} product={item} />
      )}
    </div>
  );
}

export async function getServerSideProps() {
  const docs = [
    { docType: "product", pid: "123", title: "Running Shoes" },
    { docType: "content", pid: "banner-1", contentType: "image_url", content: "https://picsum.photos/200/300" }
  ];
  return { props: { docs } };
}
const renderProduct = (product: BrProduct) => {
  return (
    <ProductCard
      key={product.pid}
      productContent={product}
      productParams={{ template: 'card', hoverEffect: false }}
    />
  );
};
const renderContentContainer = (content: any) => {
  return (
    <ProductCardCampaign
      key={content.pid}
      type={content.type}
      content={content.content}
      pid={content.pid}
    />
  );
};

Example content card component with media

export const ProductCardCampaign = (props: any) => {
  const renderCampaignContent = (type: string) => {
    switch (type) {
      case 'html':
        return <div dangerouslySetInnerHTML={{ __html: props.content }} />;
      case 'image_url': {
        let imageSrc = props.content;
        if (props.content?.trim().startsWith('<img')) {
          const match = props.content.match(/src=["']([^"']+)["']/i);
          if (match?.[1]) imageSrc = match[1];
        }
        return (
          <img
            src={imageSrc}
            alt={props.alt || ''}
            style={{ width: '200px', maxHeight: '355px', objectFit: 'contain' }}
          />
        );
      }
      default:
        return <div>{props.content}</div>;
    }
  };

  return (
    <StyledBazaarCard hoverEffect={false} data-type="product-card">
      <FlexBox flexDirection="column" sx={{ justifyContent: 'center', textAlign: 'center' }}>
        <FlexBox sx={{ height: '355px', overflow: 'hidden' }} justifyContent="center" alignItems="center">
          {renderCampaignContent(props.type)}
        </FlexBox>
      </FlexBox>
    </StyledBazaarCard>
  );
};
import React from "react";

interface ProductCardCampaignProps {
  type: string;
  content: string;
  alt?: string;
}

export const ProductCardCampaign: React.FC<ProductCardCampaignProps> = ({ type, content, alt }) => {
  const renderCampaignContent = (type: string) => {
    switch (type) {
      case "html":
        return <div dangerouslySetInnerHTML={{ __html: content }} />;
      case "image_url": {
        let imageSrc = content;
        if (content?.trim().startsWith("<img")) {
          const match = content.match(/src=["']([^"']+)["']/i);
          if (match?.[1]) imageSrc = match[1];
        }
        return <img src={imageSrc} alt={alt || ""} style={{ width: "200px", maxHeight: "355px", objectFit: "contain" }} />;
      }
      default:
        return <div>{content}</div>;
    }
  };

  return (
    <div className="bazaar-card" data-type="product-card">
      <div style={{ display: "flex", flexDirection: "column", textAlign: "center" }}>
        <div style={{ height: "355px", overflow: "hidden", display: "flex", justifyContent: "center", alignItems: "center" }}>
          {renderCampaignContent(type)}
        </div>
      </div>
    </div>
  );
};
import React from "react";

export const ProductCardCampaign = (props) => {
  const renderCampaignContent = (type) => {
    switch (type) {
      case "html":
        return <div dangerouslySetInnerHTML={{ __html: props.content }} />;
      case "image_url": {
        let imageSrc = props.content;
        if (props.content?.trim().startsWith("<img")) {
          const match = props.content.match(/src=["']([^"']+)["']/i);
          if (match?.[1]) imageSrc = match[1];
        }
        return (
          <img
            src={imageSrc}
            alt={props.alt || ""}
            style={{ width: "200px", maxHeight: "355px", objectFit: "contain" }}
          />
        );
      }
      default:
        return <div>{props.content}</div>;
    }
  };

  return (
    <div className="bazaar-card" data-type="product-card">
      <div style={{ display: "flex", flexDirection: "column", textAlign: "center" }}>
        <div style={{ height: "355px", overflow: "hidden", display: "flex", justifyContent: "center", alignItems: "center" }}>
          {renderCampaignContent(props.type)}
        </div>
      </div>
    </div>
  );
};
<script setup lang="ts">
import { defineProps } from "vue";

const props = defineProps<{
  type: string;
  content: string;
  alt?: string;
}>();

const renderCampaignContent = () => {
  switch (props.type) {
    case "html":
      return props.content; // v-html will render safely
    case "image_url": {
      let imageSrc = props.content;
      if (props.content?.trim().startsWith("<img")) {
        const match = props.content.match(/src=["']([^"']+)["']/i);
        if (match?.[1]) imageSrc = match[1];
      }
      return `<img src="${imageSrc}" alt="${props.alt || ""}" style="width:200px;max-height:355px;object-fit:contain"/>`;
    }
    default:
      return props.content;
  }
};
</script>

<template>
  <div class="bazaar-card" data-type="product-card">
    <div style="display:flex;flex-direction:column;text-align:center">
      <div style="height:355px;overflow:hidden;display:flex;justify-content:center;align-items:center" v-html="renderCampaignContent()" />
    </div>
  </div>
</template>
// product-card-campaign.component.ts

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-product-card-campaign',
  templateUrl: './product-card-campaign.component.html',
  styleUrls: ['./product-card-campaign.component.css']
})
export class ProductCardCampaignComponent {
  @Input() type!: string;
  @Input() content!: string;
  @Input() alt?: string;

  getContent() {
    switch (this.type) {
      case 'html':
        return this.content;
      case 'image_url': {
        let imageSrc = this.content;
        const match = this.content?.match(/src=["']([^"']+)["']/i);
        if (match?.[1]) imageSrc = match[1];
        return `<img src="${imageSrc}" alt="${this.alt || ''}" style="width:200px;max-height:355px;object-fit:contain"/>`;
      }
      default:
        return this.content;
    }
  }
}

// product-card-campaign.component.html

<div class="bazaar-card" data-type="product-card">
  <div style="display:flex;flex-direction:column;text-align:center">
    <div style="height:355px;overflow:hidden;display:flex;justify-content:center;align-items:center"
         [innerHTML]="getContent()">
    </div>
  </div>
</div>
// app/components/ProductCardCampaign.tsx

"use client";
import React from "react";

interface Props {
  type: string;
  content: string;
  alt?: string;
}

export default function ProductCardCampaign({ type, content, alt }: Props) {
  const renderCampaignContent = (type: string) => {
    switch (type) {
      case "html":
        return <div dangerouslySetInnerHTML={{ __html: content }} />;
      case "image_url": {
        let imageSrc = content;
        if (content?.trim().startsWith("<img")) {
          const match = content.match(/src=["']([^"']+)["']/i);
          if (match?.[1]) imageSrc = match[1];
        }
        return <img src={imageSrc} alt={alt || ""} style={{ width: "200px", maxHeight: "355px", objectFit: "contain" }} />;
      }
      default:
        return <div>{content}</div>;
    }
  };

  return (
    <div className="bazaar-card" data-type="product-card">
      <div style={{ display: "flex", flexDirection: "column", textAlign: "center" }}>
        <div style={{ height: "355px", overflow: "hidden", display: "flex", justifyContent: "center", alignItems: "center" }}>
          {renderCampaignContent(type)}
        </div>
      </div>
    </div>
  );
}

// Usage inside a Next.js page:

import ProductCardCampaign from "@/components/ProductCardCampaign";

export default function Page() {
  const mockContent = { type: "image_url", content: "https://via.placeholder.com/200" };

  return <ProductCardCampaign {...mockContent} />;
}