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:
- Pass the feature parameter in the API request to get media.
- Identify the objects containing media between the product results.
- Set the appropriate display (for static types like images) or rendering conditions (for HTML or text assets) based on the media type.
- 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=falsein 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": Thecontentfield contains a URL to a plain text file (UTF-8 encoded). -
contentType: "html_file": Thecontentfield 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
contentfield and pass the resolved content string to your view or template. -
Client-side (for dynamic apps or when SEO is less critical): Use
fetchoraxiosin 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
dangerouslySetInnerHTMLonly 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.
Updated 15 days ago
