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.comdomain exposed asPUBLIC_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
| Symptom | Cause 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 blocked | A custom script-src replaced Hydrogen's defaults. Use the defaultSrc extension above. |
| Agent, analytics or chat requests are blocked | Add https://agent.getjust.eu and https://agent-stream.getjust.eu to connectSrc. |
| The configured widget font falls back | Add https://fonts.gstatic.com to fontSrc. |
| Global CSS or Global JS is blocked | Check 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 disappears | Check 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 render | A browser extension may have changed the HTML before React hydrated it. Re-test in a clean profile or private window. |
Test the integration
- Reload the storefront and confirm the trigger bar appears.
- Add from the chat on an empty cart and confirm
/cartreceives the request. - Confirm
cart-idincludes the Storefront API?key=parameter unchanged. - Open the cart from the widget and confirm your Hydrogen cart UI opens above it.
- Decline analytics consent and confirm chat still works without tracking requests.