JUST Docs
WidgetInstallationHeadless storefronts

Hydrogen

Add the JUST widget to Shopify Hydrogen, including its CSP and CartForm bridge.

Hydrogen already owns the Shopify storefront context and cart. The integration publishes that state to one <just-ai-agent> element mounted in the root layout, then delegates widget cart requests to Hydrogen's /cart action.

Prerequisites

  • The JUST Shopify app installed and connected on the store.
  • The exact *.myshopify.com domain exposed as PUBLIC_STORE_DOMAIN.
  • A Hydrogen cart action using CartForm, as provided by the skeleton.

Load the module and mount the element

Use Hydrogen's Script component in the root layout. It adds the CSP nonce automatically:

import {Script} from '@shopify/hydrogen';

<Script
  type="module"
  src="https://agent.getjust.eu/ai-commerce-agent-widget.js"
/>

<just-ai-agent
  shop={data.publicStoreDomain}
  headless="true"
  locale={data.locale.toLowerCase()}
  country={data.country}
/>

Expose publicStoreDomain, storefront.i18n.language as locale, and storefront.i18n.country as country from the root loader. Keep both nodes mounted once, outside the route outlet.

For a logged-in shopper, also set customer-email from the Customer Account API, so guided flows skip asking for an email the shopper already gave you. customer-id currently has no effect on a headless storefront: the agent only recognizes a customer Shopify confirms, which needs a Shopify theme.

Load the JUST widget as a module immediately

Do not use waitForHydration. Hydrogen's delayed script loader can treat this ESM bundle as a classic script, which fails on its final export statement with Unexpected token 'export'.

Extend Hydrogen's Content Security Policy

Add the widget origin to both resource loading and network connections in app/entry.server.tsx. Keep the existing shop configuration:

const { nonce, header, NonceProvider } = createContentSecurityPolicy({
  defaultSrc: ['https://agent.getjust.eu'],
  connectSrc: ['https://agent.getjust.eu', 'https://agent-stream.getjust.eu'],
  fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
  shop: {
    checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
    storeDomain: context.env.PUBLIC_STORE_DOMAIN,
  },
});

Use defaultSrc here so Hydrogen merges the widget origin into its defaults. Defining only scriptSrc: ['https://agent.getjust.eu'] creates a narrower directive that blocks Hydrogen's own route modules and Shopify's consent script.

Hydrogen's default style-src permits inline styles. Preserve that behavior if you replace the directive: the JUST widget renders styles in its Shadow DOM and can add a shop-level <style data-just-ai="custom"> to the document. A configured Global JS override is injected as an inline <script data-just-ai="custom-js">; a strict nonce-only policy blocks that optional override because it has no host nonce. The widget itself does not require Global JS.

Add the JSX type

TypeScript does not know the custom element until the project declares it. Add this to env.d.ts:

import type { DetailedHTMLProps, HTMLAttributes } from 'react';

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'just-ai-agent': DetailedHTMLProps<
        HTMLAttributes<HTMLElement>,
        HTMLElement
      > & {
        shop: string;
        headless: 'true';
        locale: string;
        country: string;
        'cart-count'?: string;
        'cart-id'?: string;
        'tracking-consent'?: 'true' | 'false';
        'customer-email'?: string;
        'customer-id'?: string;
      };
    }
  }
}

Bridge Hydrogen's cart

The widget dispatches cart-add-requested on its standalone element. Submit the requested line to the existing Hydrogen cart action:

import { CartForm } from '@shopify/hydrogen';
import { useEffect, useRef } from 'react';
import { useFetcher } from 'react-router';

type AddRequest = { variantId: string; quantity: number };
type CartActionData = {
  cart?: { id: string; totalQuantity: number } | null;
  errors?: unknown[];
};

const widget = () =>
  document.querySelector<HTMLElement>('just-ai-agent:not([mode="block"])');

export function HydrogenCartBridge() {
  const fetcher = useFetcher<CartActionData>();
  const pending = useRef<AddRequest | null>(null);

  useEffect(() => {
    const element = widget();
    if (!element) return;

    const onAddRequested = (event: Event) => {
      const { variantId, quantity } = (event as CustomEvent<AddRequest>).detail;
      pending.current = { variantId, quantity };

      void fetcher.submit(
        {
          [CartForm.INPUT_NAME]: JSON.stringify({
            action: CartForm.ACTIONS.LinesAdd,
            inputs: { lines: [{ merchandiseId: variantId, quantity }] },
          }),
        },
        { method: 'POST', action: '/cart' },
      );
    };

    element.addEventListener('cart-add-requested', onAddRequested);
    return () =>
      element.removeEventListener('cart-add-requested', onAddRequested);
  }, [fetcher]);

  useEffect(() => {
    if (fetcher.state !== 'idle' || !fetcher.data || !pending.current) return;

    const added = pending.current;
    pending.current = null;
    if (fetcher.data.errors?.length || !fetcher.data.cart) return;

    widget()?.setAttribute('cart-id', fetcher.data.cart.id);
    widget()?.setAttribute(
      'cart-count',
      String(fetcher.data.cart.totalQuantity),
    );
    widget()?.dispatchEvent(new CustomEvent('cart-added', { detail: added }));
  }, [fetcher.data, fetcher.state]);

  return null;
}

Also publish cart-count and the verbatim Storefront API cart-id whenever the root cart changes. Listen for cart-clicked in the same bridge, open your cart drawer, then dispatch just:ai-agent:close. The complete payload and ordering rules are in the events reference, and Analytics explains why every successful storefront add must dispatch cart-added.

Troubleshooting

SymptomCause and fix
Unexpected token 'export'The bundle was parsed as a classic script. Keep type="module" and remove waitForHydration.
Hydrogen route modules or Shopify consent scripts are blockedA custom script-src replaced Hydrogen's defaults. Use the defaultSrc extension above.
Agent, analytics or chat requests are blockedAdd https://agent.getjust.eu and https://agent-stream.getjust.eu to connectSrc.
The configured widget font falls backAdd https://fonts.gstatic.com to fontSrc.
Global CSS or Global JS is blockedCheck the host's inline style/script policy. Global JS is optional and cannot run under a nonce-only CSP today.
The script loads but the widget disappearsCheck the console for Shop not found, then verify the exact shop domain and that the store is enabled in the same JUST environment.
Hydration fails on an element your app does not renderA browser extension may have changed the HTML before React hydrated it. Re-test in a clean profile or private window.

Test the integration

  1. Reload the storefront and confirm the trigger bar appears.
  2. Add from the chat on an empty cart and confirm /cart receives the request.
  3. Confirm cart-id includes the Storefront API ?key= parameter unchanged.
  4. Open the cart from the widget and confirm your Hydrogen cart UI opens above it.
  5. Decline analytics consent and confirm chat still works without tracking requests.

On this page