@personizely/shopify-hydrogen
v1.0.3
Published
Personizely adapter for Shopify Hydrogen headless storefronts - Simple React Provider integration
Maintainers
Readme
@personizely/shopify-hydrogen
Personizely adapter for Shopify Hydrogen headless storefronts. Simple React Provider integration for personalization, popups, and A/B testing.
Features
- ✅ Plug & Play - Minimal configuration required
- ✅ Server-Side Script Loading - Prevents A/B test flicker
- ✅ Fully Reactive - Integrates seamlessly with Hydrogen's cart
- ✅ TypeScript - Fully typed for great DX
- ✅ Auto Page Detection - Automatically detects product/collection pages
- ✅ Cart & Checkout Triggers - Easy widget triggering
Installation
npm install @personizely/shopify-hydrogenQuick Start
1. Set Up Your Root Loader
In your app/root.tsx, add Personizely configuration to the loader:
export async function loader(args: Route.LoaderArgs) {
const { env } = args.context
return {
// ... other data
personizely: {
storefrontAccessToken: env.PUBLIC_STOREFRONT_API_TOKEN,
shopDomain: env.PUBLIC_STORE_DOMAIN,
}
}
}2. Wrap Your App with PersonizelyProvider
In your app/root.tsx default export:
import { PersonizelyProvider } from '@personizely/shopify-hydrogen'
export default function App() {
const data = useRouteLoaderData<RootLoader>('root')
if (!data) {
return <Outlet />
}
return (
<PersonizelyProvider
websiteApiKey="your-api-key"
storefrontAccessToken={data.personizely.storefrontAccessToken}
shopDomain={data.personizely.shopDomain}
locale="en-US"
>
<Outlet />
</PersonizelyProvider>
)
}That's it! The Provider:
- ✅ Automatically integrates with Hydrogen's cart from the root loader
- ✅ Sets up the config before loading the script
- ✅ Loads the Personizely snippet dynamically
- ✅ Provides hooks for triggering widgets
3. Trigger Cart Add Events
In your product form component:
import { useCartAdd } from '@personizely/shopify-hydrogen'
export function ProductForm({ product, selectedVariant }) {
const triggerCartAdd = useCartAdd()
const handleAddToCart = async () => {
// Trigger Personizely widget (e.g., upsell popup)
const widgetShown = await triggerCartAdd({
id: product.id,
variantId: selectedVariant.id,
price: parseFloat(selectedVariant.price.amount),
quantity: 1,
handle: product.handle,
})
if (!widgetShown) {
// Add to cart normally if no widget was shown
// Your cart add logic here
}
}
return (
<button onClick={handleAddToCart}>
Add to Cart
</button>
)
}4. Trigger Checkout Events
In your cart or checkout button component:
import { useCheckout } from '@personizely/shopify-hydrogen'
export function CheckoutButton({ checkoutUrl }) {
const triggerCheckout = useCheckout()
const handleCheckout = async () => {
// Trigger Personizely widget (e.g., exit intent offer)
const widgetShown = await triggerCheckout()
if (!widgetShown) {
// Proceed to checkout if no widget was shown
window.location.href = checkoutUrl
}
}
return (
<button onClick={handleCheckout}>
Proceed to Checkout
</button>
)
}5. Set Page Context
Use usePageContext to inform Personizely about the current page for better targeting:
import { usePageContext } from '@personizely/shopify-hydrogen'
export default function ProductRoute() {
const { product } = useLoaderData<typeof loader>()
// Set page context on product pages
usePageContext({
pageType: 'product',
product: {
id: extractId(product.id), // Numeric ID
handle: product.handle,
tags: product.tags
}
})
return <ProductPage product={product} />
}When to use usePageContext:
- Product pages - Set
pageType: 'product'with product data - Collection pages - Set
pageType: 'collection'with collection data - Home page - Set
pageType: 'home' - Cart page - Set
pageType: 'cart'
Example for collection page:
usePageContext({
pageType: 'collection',
collection: {
id: extractId(collection.id)
}
})API Reference
Components
<PersonizelyProvider>
React context provider for Personizely integration.
Props:
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| websiteApiKey | string | ✅ | Your Personizely website API key |
| storefrontAccessToken | string | ✅ | Shopify Storefront API access token |
| shopDomain | string | ✅ | Your shop domain (e.g., 'myshop.myshopify.com') |
| locale | string | ❌ | Shop locale (default: 'en-US') |
| currency | object | ❌ | Currency configuration (see below) |
| market | string | ❌ | Market handle for international stores |
| apiVersion | string | ❌ | Storefront API version (default: '2024-01') |
| methods | object | ❌ | Custom method overrides (see below) |
| customer | Customer \| null | ❌ | Customer object for personalization |
Currency Configuration:
currency={{
rate: 1.2, // Conversion rate from base currency
active: 'EUR', // Current currency code
base: 'USD' // Base currency code
}}Methods Configuration:
methods={{
// Custom money formatting
formatMoney: (amount, includeDecimals) => {
// amount is in cents
// Return formatted string (e.g., "$19.99" or "$20")
return includeDecimals
? `$${(amount / 100).toFixed(2)}`
: `$${Math.round(amount / 100)}`
},
// Custom product URL builder
builtProductPath: ({ handle }) => {
// Return product URL path
return `/products/${handle}`
}
}}Complete Example:
<PersonizelyProvider
websiteApiKey="your-api-key"
storefrontAccessToken={data.personizely.storefrontAccessToken}
shopDomain={data.personizely.shopDomain}
locale="en-US"
currency={{
rate: 0.85,
active: 'EUR',
base: 'USD'
}}
market="europe"
methods={{
formatMoney: (amount, includeDecimals) => {
const value = amount / 100
return includeDecimals
? `€${value.toFixed(2).replace('.', ',')}`
: `€${Math.round(value)}`
},
builtProductPath: ({ handle }) => `/shop/${handle}`
}}
customer={customer}
>
{children}
</PersonizelyProvider>Hooks
useCartAdd()
Returns a function to trigger cart add widgets (e.g., upsells, cross-sells).
Returns: (product: CartAddProduct) => Promise<boolean>
- Returns
trueif a widget was shown - Returns
falseif no widget was shown
CartAddProduct Type:
{
id: string | number // Product ID
variantId: string | number // Variant ID
price?: string | number // Price in dollars (will be converted to cents)
quantity?: number // Quantity (default: 1)
handle?: string // Product handle
properties?: Record<string, string> // Custom properties
}Example:
const triggerCartAdd = useCartAdd()
const widgetShown = await triggerCartAdd({
id: 'gid://shopify/Product/123',
variantId: 'gid://shopify/ProductVariant/456',
price: 19.99, // In dollars
quantity: 1,
handle: 'awesome-product',
properties: {
gift_wrap: 'yes'
}
})useCheckout()
Returns a function to trigger checkout widgets (e.g., exit intent offers, discount popups).
Returns: () => Promise<boolean>
- Returns
trueif a widget was shown - Returns
falseif no widget was shown
Example:
const triggerCheckout = useCheckout()
const widgetShown = await triggerCheckout()
if (!widgetShown) {
// Proceed with checkout
window.location.href = checkoutUrl
}usePageContext(context)
Sets the current page context for better widget targeting. Call this hook in your route components.
Parameters:
context: Partial<PageContext>- Page context object
PageContext Type:
{
pageType?: string | number // Page type: 'product' | 'collection' | 'home' | 'cart' | custom
product?: { // Product data (for product pages)
id: number // Numeric product ID
handle: string // Product handle
tags?: string[] // Product tags
} | null
collection?: { // Collection data (for collection pages)
id: number // Numeric collection ID
} | null
}Examples:
Product page:
import { usePageContext } from '@personizely/shopify-hydrogen'
export default function ProductRoute() {
const { product } = useLoaderData<typeof loader>()
usePageContext({
pageType: 'product',
product: {
id: extractNumericId(product.id), // Extract numeric ID from GID
handle: product.handle,
tags: product.tags
}
})
return <ProductPage product={product} />
}Collection page:
usePageContext({
pageType: 'collection',
collection: {
id: extractNumericId(collection.id)
}
})Home page:
usePageContext({
pageType: 'home'
})Cart page:
usePageContext({
pageType: 'cart'
})usePersonizely()
Returns the Personizely context with cart and adapter instances.
Returns: { cart: Cart, adapter: Adapter, websiteApiKey: string }
Example:
const { cart, adapter, websiteApiKey } = usePersonizely()
// Access cart methods
const cartTotal = cart.getValue()
const itemCount = cart.getSize()
// Access adapter methods
const formattedPrice = adapter.formatMoney(1999, true) // "$19.99"
const productUrl = adapter.buildProductPath({ handle: 'my-product' })
// Get customer data (returns what was set via the customer prop)
const customer = adapter.getCustomer()
if (customer) {
console.log('Customer:', customer.displayName, customer.tags)
}Advanced Usage
Multi-Currency Support
// Define currency outside component for stable reference
const CURRENCY = {
rate: 0.85, // EUR/USD rate
active: 'EUR', // Display currency
base: 'USD' // Base currency
}
export default function App() {
return (
<PersonizelyProvider
websiteApiKey="your-api-key"
storefrontAccessToken={token}
shopDomain={domain}
currency={CURRENCY}
methods={{
formatMoney: (amount, includeDecimals) => {
const value = amount / 100
return includeDecimals
? `€${value.toFixed(2).replace('.', ',')}`
: `€${Math.round(value)}`
}
}}
>
{children}
</PersonizelyProvider>
)
}International Markets
// Define market outside component for stable reference
const MARKET = 'europe'
export default function App() {
return (
<PersonizelyProvider
websiteApiKey="your-api-key"
storefrontAccessToken={token}
shopDomain={domain}
market={MARKET}
locale="de-DE"
>
{children}
</PersonizelyProvider>
)
}Authenticated Customers
Pass customer data directly to enable customer-specific personalization:
export default function App() {
const data = useRouteLoaderData<RootLoader>('root')
// Get customer data from your loader or Hydrogen's customer account
const customer = data.customer ? {
id: data.customer.id,
email: data.customer.email,
firstName: data.customer.firstName,
lastName: data.customer.lastName,
displayName: data.customer.displayName,
hasAccount: true,
tags: data.customer.tags || []
} : null
return (
<PersonizelyProvider
websiteApiKey="your-api-key"
storefrontAccessToken={data.personizely.storefrontAccessToken}
shopDomain={data.personizely.shopDomain}
customer={customer}
>
{children}
</PersonizelyProvider>
)
}Customer Type:
type Customer = {
id: string
email?: string | null
phone?: string | null
firstName?: string | null
lastName?: string | null
displayName: string
hasAccount: boolean
tags: string[]
}Custom Product URLs
<PersonizelyProvider
websiteApiKey="your-api-key"
storefrontAccessToken={token}
shopDomain={domain}
methods={{
builtProductPath: ({ handle }) => {
// Custom URL structure
return `/shop/products/${handle}`
}
}}
>
{children}
</PersonizelyProvider>TypeScript
This package is fully typed. Import types as needed:
import type {
CartAddProduct,
PageContext,
PersonizelyConfigMethods,
Product,
Customer,
} from '@personizely/shopify-hydrogen'How It Works
- Provider Setup:
PersonizelyProvidersets upwindow.__PLY_HYDROGEN_CONFIG__with cart and adapter instances - Script Loading: Provider dynamically loads the Personizely snippet after config is ready
- Cart Integration: Automatically integrates with Hydrogen's cart from the root loader
- Page Context: Use
usePageContext()in route components to set page type and data - Event Triggers:
useCartAddanduseCheckouthooks trigger widgets at key moments
Compatibility
- Shopify Hydrogen: v2.x
- React Router: v7.x
- React: v18.x
- TypeScript: v5.x
Getting Your API Keys
- Personizely Website API Key: Get this from your Personizely dashboard
- Shopify Storefront Access Token: Create a custom app in Shopify Admin with Storefront API access
Support
- 📧 Email: [email protected]
- 📚 Docs: https://help.personizely.net
- 💬 Chat: Available in your Personizely dashboard
License
MIT
