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.

📘

Note

To learn how to configure or manage assets and media rules in the Bloomreach dashboard, refer to the Display media in the grid guide.

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.

🚧

Important

Media can also appear inside recommendation and pathways widgets as they use the same Search API to fetch products.

If you wish to disable media in widgets, pass content_injection=false in the widget API calls.

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": "<img src=\"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 sanitize the media information from the API response to display and render the assets as required.

Below is an example of the logic and components to use for displaying product cards alongside 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':
      case 'weblayer':
      case 'clarity':
        const isClarity = props.content.type === 'clarity';
        return <Grid item md={gridSize} xs={12} sm={6} key={props.content.pid} sx={
          isClarity
            ? { flexBasis: '50% !important', maxWidth: '50% !important' }
            : {}
        }>
          <ProductCardCampaign type={props.content.type} content={props.content.content} pid={props.content.pid} />
        </Grid>;
      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":
      case "weblayer":
      case "clarity":
        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":
      case "weblayer":
      case "clarity":
        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":
    case "weblayer":
    case "clarity":
      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':
      case 'weblayer':
      case 'clarity':
        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":
      case "weblayer":
      case "clarity":
        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} />;
}

Handle large HTML and text asset files

When a media asset's content exceeds 2000 characters, Bloomreach returns a CDN URL rather than the inline content. The contentType changes to text_file or html_file to indicate file-based delivery.

This approach supports very large media assets without impacting API payload size, while keeping your product grid fast, safe, and flexible.

When file-based content is used

If the asset body (HTML or plain text) exceeds 2000 characters, the Search API responds with entries like:

  • contentType: "text_file": The content field contains a URL to a plain text file (UTF-8 encoded). 

  • contentType: "html_file": The content field contains a URL to an HTML document.

{
  ...
    {
      "contentType": "text_file",
      "content": "https://sitesearch-static-cdn-dev.bloomreach.com/tmp/sandbox_product01/1769598249313-sandbox_product01-content.txt",
      "metaInfo": "",
      "docType": "content"
    },
    {
      "contentType": "html_file",
      "content": "https://sitesearch-static-cdn-dev.bloomreach.com/tmp/sandbox_product01/1769598352470-sandbox_product01-content.html",
      "metaInfo": "",
      "docType": "content"
    }
}

The docType: "content" behavior and media rule logic remain unchanged. Only the delivery mechanism shifts to file-based URLs.

Render text_file and html_file content

You must fetch and render the file content in your application, similar to how you handle inline text or html content.

Detect file-based content types

In your grid rendering logic, check for CONTENT docs with *_file types and handle them accordingly:

if (item.docType === "content") {
  switch (item.contentType) {
    case "text":
    case "html":
    case "image_url":
      return renderInlineContent(item);

    case "text_file":
    case "html_file":
      return renderFileBackedContent(item);

    default:
      return renderFallbackContent(item);
  }
} else {
  return renderProduct(item);
}

Fetch the file content

You can fetch file content using either approach:

  • Server-side (recommended for server-side rendering and SEO-sensitive pages): Fetch the file from the URL in the content field and pass the resolved content string to your view or template.

  • Client-side (for dynamic apps or when SEO is less critical): Use fetch or axios in your component to load the file content on mount, then render it once available.

Example

type FileContentDoc = {
  pid: string;
  docType: "content";
  contentType: "text_file" | "html_file";
  content: string; // URL
  metaInfo?: string;
};

function FileBackedContentCard({ doc }: { doc: FileContentDoc }) {
  const [body, setBody] = React.useState<string | null>(null);
  const [error, setError] = React.useState<string | null>(null);

  React.useEffect(() => {
    fetch(doc.content)
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.text();
      })
      .then(setBody)
      .catch((e) => setError(e.message));
  }, [doc.content]);

  if (error) {
    return <div className="content-error">Unable to load content.</div>;
  }

  if (!body) {
    return <div className="content-loading">Loading…</div>;
  }

  if (doc.contentType === "text_file") {
    // Treat as plain text, optionally add styling or formatting
    return <div className="content-text">{body}</div>;
  }

  // html_file: sanitize before injecting into DOM
  const safeHtml = sanitizeHtml(body); // use your preferred sanitization library
  return <div className="content-html" dangerouslySetInnerHTML={{ __html: safeHtml }} />;
}

Integrate this with the conditional rendering logic in the ProductCardCampaign example above.

File rendering guidelines

For text_file content

  • Render as plain text inside a <div> or <p> element.

  • Apply your grid styles and formatting.

  • Don't interpret as HTML, and escape special characters if needed.

  • Use for long-form text, such as disclaimers, guides, or campaign messages.

For html_file content

  • Always sanitize HTML before injecting into the DOM to prevent XSS attacks.

  • Use dangerouslySetInnerHTML only after sanitization in React, or safe HTML bindings in Vue/Angular.

  • Use for rich experiences like buying guides, promotional tiles, or weblayer content from Bloomreach Engagement.

Performance and reliability

File URLs are served through Bloomreach's static CDN over HTTPS, optimized for delivery and caching.

Caching

Standard HTTP caching semantics (ETag, Cache-Control) apply at your client. For server-side rendering, consider caching resolved content for a short time to avoid repeated fetches per request.

Best practices

  • Show loading states while fetching content.

  • Provide fallback messages if fetches fail.

  • Cache resolved content server-side for SSR to reduce repeated fetches.

  • Ensure failures don't break grid rendering.

A/B testing and previews

File-backed assets behave and preview the same as inline assets in Product grid insights and A/B tests. They only use a different delivery mechanism.