> ## Documentation Index
> Fetch the complete documentation index at: https://docs.glood.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Recommendations Example

> Complete working example: client-side recommendation sections with attribution tracking

# Recommendations Example

A complete, tested integration showing recommendations on a product page and the homepage, with full attribution tracking. The primary path renders **client-side** with the `useRecommendations` hook. Copy the pieces you need.

## 1. Client Setup

Create the client once and register the recommendations app:

```tsx theme={null}
// 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',
  version: 3, // headless API version (2 | 3); 3 is the default
  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`). A **v3**
client fires `POST /v3/headless/init` automatically on mount:

```tsx theme={null}
// 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 (client-side)

`useRecommendations(params, options?)` fetches sections in the browser via the
recommendations app from `GloodProvider`. Because the fetch runs client-side,
`pageUrl` / `locale` / `clientId` / `userId` default from the browser and the
assigned experience is persisted automatically.

The hook also returns four attribution helpers. Fire them against a section's
`section_serve_id`:

* `trackRender` → when the section mounts in the DOM
* `trackView` → when the section scrolls into the viewport
* `trackClick` → when a recommended product link is clicked
* `trackAddToCart` → when the product is added to cart

```tsx theme={null}
// app/components/GloodRecommendations.tsx
import { useEffect, useRef } 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,
    trackView,
    trackClick,
    trackAddToCart,
  } = useRecommendations({ pageType, productId, cartProductIds });

  // Fire render/view once per section serve — effects can re-run and observers
  // can re-intersect; these sets keep each event to exactly one send.
  const renderedServeIds = useRef(new Set<string>());
  const viewedServeIds = useRef(new Set<string>());
  const sectionRefs = useRef(new Map<string, HTMLElement>());

  const buildTrack = (
    section: (typeof sections)[number],
    products?: Array<{ productId: string; variantId?: string; quantity?: number }>,
  ) => ({
    section: String(section.id),
    sectionServeId: section.section_serve_id,
    requestId: data?.request_id,
    page: pageType,
    parent: productId ? { productId } : undefined,
    ...(products ? { products } : {}),
  });

  // 1) glood:section:render — once per section on mount
  useEffect(() => {
    sections.forEach((section) => {
      if (renderedServeIds.current.has(section.section_serve_id)) return;
      renderedServeIds.current.add(section.section_serve_id);
      trackRender(buildTrack(section));
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sections]);

  // 2) glood:section:view — when 50% of a section enters the viewport
  useEffect(() => {
    if (typeof IntersectionObserver === 'undefined' || sections.length === 0) {
      return;
    }

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (!entry.isIntersecting) return;
          const serveId = (entry.target as HTMLElement).dataset.gloodServeId;
          if (!serveId || viewedServeIds.current.has(serveId)) return;
          viewedServeIds.current.add(serveId);
          observer.unobserve(entry.target);

          const section = sections.find((s) => s.section_serve_id === serveId);
          if (section) trackView(buildTrack(section));
        });
      },
      { threshold: 0.5 },
    );

    sectionRefs.current.forEach((el) => el && observer.observe(el));
    return () => observer.disconnect();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [sections]);

  if (loading) return <p>Loading recommendations…</p>;
  if (error) return <p>Recommendations error: {error.message}</p>;
  if (sections.length === 0) return null;

  return (
    <div className="glood-recommendations">
      {sections.map((section) => (
        <section
          key={section.section_serve_id}
          data-glood-serve-id={section.section_serve_id}
          ref={(el) => {
            if (el) sectionRefs.current.set(section.section_serve_id, el);
            else sectionRefs.current.delete(section.section_serve_id);
          }}
        >
          <h2>{section.title}</h2>
          <div className="product-grid">
            {section.products.map((product) => {
              const track = buildTrack(section, [
                {
                  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}`}
                    // glood:section:click — fires before navigation; the event
                    // uses keepalive so it survives the page unload
                    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):

```tsx theme={null}
// 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:**

```tsx theme={null}
// app/routes/_index.tsx
<GloodRecommendations pageType="home" />
```

**Cart page** — pass what's in the cart so Glood can exclude/complement it:

```tsx theme={null}
<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 with
`fetchRecommendationSections` instead of the hook:

```tsx theme={null}
// app/routes/products.$handle.tsx
import { fetchRecommendationSections } from '@glood/hydrogen';

export async function loader({ request, params, context }) {
  // Load the product first so we have its GID — the sections API wants the
  // numeric product id, not the route's `$handle` param.
  const product = await loadProduct(context, params);

  const recommendations = await fetchRecommendationSections(
    {
      apiKey: context.env.GLOOD_API_KEY,
      myShopifyDomain: 'your-store.myshopify.com',
      version: 3, // defaults to 3
    },
    {
      pageType: 'product_page',
      pageUrl: request.url,
      locale: 'en-US',
      // Derive the numeric id from the product GID (gid://shopify/Product/123)
      productId: product.id.split('/').pop(),
    },
  ).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>
      ))}
    </>
  );
}
```

<Note>
  With SSR you still get attribution: retrieve the track helpers client-side from
  `useRecommendations` (pass `{ skip: true }` to reuse the helpers without a
  second fetch) and call `trackRender` / `trackView` / `trackClick` /
  `trackAddToCart` with the `section_serve_id` values from your loader data.
</Note>

## 5. Don't Forget the CSP

Recommendation fetches and events are blocked by Hydrogen's default Content
Security Policy. Add Glood's endpoint in `entry.server`:

```tsx theme={null}
const { nonce, header, NonceProvider } = createContentSecurityPolicy({
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
  connectSrc: [
    "'self'",
    'https://storefront.glood.ai', // headless suite: init, sections, events
  ],
});
```

See [Content Security Policy](/for-developers/glood-hydrogen-sdk/content-security-policy) for details.

## What You'll See in the Console (debug: true)

```
[Glood Debug] Registered recommendations app
[Glood Debug] Running headless init: https://storefront.glood.ai/api/storefront/v3/headless/init
[Glood Debug] Fetching sections: https://storefront.glood.ai/api/storefront/v3/headless/sections {page_type: 'home', ...}
[Glood Debug] Received 5 sections
[Glood Debug] Consent granted for recommendations, processing event
[Glood Debug] Successfully sent event to https://storefront.glood.ai/api/storefront/v3/headless/events
```

## See Also

* [Loading Recommendations Guide](/for-developers/glood-hydrogen-sdk/recommendations)
* [Recommendations API Reference](/for-developers/glood-hydrogen-sdk/api-reference/recommendations-api)
* [Basic Setup Example](/for-developers/glood-hydrogen-sdk/examples/basic-setup)
