Skip to main content

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