Recommendations Example
A complete, tested integration showing recommendations on a product page and the homepage, with full attribution tracking. Copy the pieces you need.
1. Client Setup
Create the client once and register the recommendations app:
// app/lib/glood.ts
import { createGlood, recommendations } from '@glood/hydrogen';
export const glood = createGlood({
apiKey: 'gl_sf_your_storefront_token',
myShopifyDomain: 'your-store.myshopify.com',
debug: import.meta.env.DEV, // verbose [Glood Debug] console logs in dev
}).use(recommendations());
Wrap your app in root.tsx (inside Shopify’s Analytics.Provider):
// app/root.tsx
import { Analytics } from '@shopify/hydrogen';
import { GloodProvider } from '@glood/hydrogen';
import { glood } from '~/lib/glood';
export default function App() {
const data = useRouteLoaderData('root');
return (
<Analytics.Provider cart={data.cart} shop={data.shop} consent={data.consent}>
<GloodProvider client={glood} loaderData={data}>
<PageLayout {...data}>
<Outlet />
</PageLayout>
</GloodProvider>
</Analytics.Provider>
);
}
2. Reusable Recommendations Component
One component that fetches, renders, and attributes recommendations for any page type:
// app/components/GloodRecommendations.tsx
import { useEffect } from 'react';
import { useRecommendations } from '@glood/hydrogen';
export function GloodRecommendations({
pageType,
productId,
cartProductIds,
}: {
pageType: string;
productId?: string;
cartProductIds?: string[];
}) {
const {
sections,
data,
loading,
error,
trackRender,
trackClick,
trackAddToCart,
} = useRecommendations({ pageType, productId, cartProductIds });
// Attribute each section render once the response arrives
useEffect(() => {
if (!data) return;
data.sections.forEach((section) => {
trackRender({
section: String(section.id),
sectionServeId: section.section_serve_id,
requestId: data.request_id,
page: pageType,
parent: productId ? { productId } : undefined,
});
});
}, [data, pageType, productId, trackRender]);
if (loading) return <p>Loading recommendations…</p>;
if (error || sections.length === 0) return null;
return (
<div className="glood-recommendations">
{sections.map((section) => (
<section key={section.section_serve_id}>
<h2>{section.title}</h2>
<div className="product-grid">
{section.products.map((product) => {
const track = {
section: String(section.id),
sectionServeId: section.section_serve_id,
requestId: data?.request_id,
page: pageType,
parent: productId ? { productId } : undefined,
products: [
{
productId: String(product.product_id),
variantId: product.variants[0]
? String(product.variants[0].variant_id)
: undefined,
quantity: 1,
},
],
};
return (
<div key={product.product_id} className="product-card">
<a
href={`/products/${product.handle}`}
onClick={() => trackClick(track)}
>
<img src={product.image?.url} alt={product.title} loading="lazy" />
<h3>{product.title}</h3>
<p>
{product.price}
{product.compare_at_price &&
product.compare_at_price > product.price && (
<s>{product.compare_at_price}</s>
)}
</p>
</a>
<button
onClick={() => {
// ...add to cart via your cart handler, then attribute:
trackAddToCart(track);
}}
>
Add to cart
</button>
</div>
);
})}
</div>
</section>
))}
</div>
);
}
3. Use It on Any Page
Product page — pass the numeric product id (strip the GID prefix):
// app/routes/products.$handle.tsx
import { GloodRecommendations } from '~/components/GloodRecommendations';
export default function Product() {
const { product } = useLoaderData();
return (
<div className="product">
{/* ...product details... */}
<GloodRecommendations
pageType="product_page"
productId={product.id.split('/').pop()}
/>
</div>
);
}
Homepage:
// app/routes/_index.tsx
<GloodRecommendations pageType="home" />
Cart page — pass what’s in the cart so Glood can exclude/complement it:
<GloodRecommendations
pageType="cart"
cartProductIds={cart.lines.nodes.map((line) =>
line.merchandise.product.id.split('/').pop(),
)}
/>
4. Server-Side Rendering Variant
To include recommendations in the initial HTML, fetch in the route loader instead:
// app/routes/products.$handle.tsx
import { fetchRecommendationSections } from '@glood/hydrogen';
export async function loader({ request, params, context }) {
const [product, recommendations] = await Promise.all([
loadProduct(context, params),
fetchRecommendationSections(
{
apiKey: context.env.GLOOD_API_KEY,
myShopifyDomain: 'your-store.myshopify.com',
},
{
pageType: 'product_page',
pageUrl: request.url,
locale: 'en-US',
productId: params.productId,
},
).catch(() => null), // degrade gracefully
]);
return { product, recommendations };
}
export default function Product() {
const { recommendations } = useLoaderData();
return (
<>
{recommendations?.sections.map((section) => (
<section key={section.section_serve_id}>
<h2>{section.title}</h2>
{/* render section.products */}
</section>
))}
</>
);
}
With SSR you still get attribution: retrieve the app client-side with client.getApp('recommendations') (or the useRecommendations track helpers) and call trackRender/trackClick with the section_serve_id values from your loader data.
5. Don’t Forget the CSP
Recommendation fetches and pixel events are blocked by Hydrogen’s default Content Security Policy. Add Glood’s domains in entry.server:
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
connectSrc: [
"'self'",
'https://storefront.glood.ai', // recommendations API
'https://events.glood.ai', // pixel events
],
});
See Content Security Policy for details.
What You’ll See in the Console (debug: true)
[Glood Debug] Registered recommendations app
[Glood Debug] Fetching sections: https://storefront.glood.ai/... {page_type: 'home', ...}
[Glood Debug] Received 5 sections
[Glood Debug] Setting up centralized subscription for page_viewed
[Glood Debug] Received page_viewed event, distributing to interested apps
[Glood Debug] Consent granted for recommendations, processing event
[Glood Debug] Successfully sent event to https://events.glood.ai/api/storefront/event
See Also