Complete guide for integrating ParcelPanel shipping tracking components into Shopify Hydrogen Storefront projects.
Hydrogen Storefront Integration (SSR)
- Create SSR-Safe Components
- Configure Content Security Policy (CSP)
- Configure Vite SSR External
- Use TrackingWidget Component in Tracking Pages
- Use EDD Component in Product Pages
Pure Client-Side Rendering Projects (CSR)
Component Overview
NPM Package: @cw-parcelpanel/headless-react
ParcelPanel provides two core components for Shopify Headless stores:
TrackingWidget- Order Tracking component (for tracking pages)ParcelPanelEDD- Estimated Delivery Date component (for product pages)
Component Features
- Shopify Native - Built specifically for the Shopify ecosystem
- Headless Ready - Perfect fit for custom storefronts and mobile apps
- Enterprise Grade - Trusted by tens of thousands of Shopify merchants
- Developer Friendly - Clear API with full TypeScript support
- Responsive Design - Automatically adapts to different screen sizes
- Multi-language Support - Built-in internationalization
- Performance Optimized - Lightweight and efficient rendering
Installation
npm install @cw-parcelpanel/headless-react⚠️ Important: SSR Compatibility Notice
If your project uses Server-Side Rendering (SSR) (like Hydrogen, Next.js App Router, Remix, etc.), you need special handling because ParcelPanel components depend on the browser's document object.
If your project is purely Client-Side Rendering (CSR) (like Create React App, Vite SPA, etc.), you can use the components directly without special handling.
Hydrogen Storefront Integration (SSR)
1. Create SSR-Safe Components
Create Client-Only Component File
Create app/components/ParcelPanelComponents.tsx:
import { useState, useEffect } from 'react'
interface TrackingWidgetProps {
shopDomain: string
locale: string
className?: string
onLoaded?: () => void
onError?: (error: any) => void
}
interface ParcelPanelEDDProps {
shopDomain: string
productId: string
locale: string
customer: {
country_code: string
province_code: string
}
onLoaded?: (sdk: any) => void
onError?: (error: any) => void
}
/**
* TrackingWidget Component - Client-side only
* Usage:
* <TrackingWidget shopDomain="..." locale="..." />
*/
export function TrackingWidget(props: TrackingWidgetProps) {
const mounted = useClientOnly()
const [Component, setComponent] = useState<any>(null)
useEffect(() => {
if (!mounted) return
// Dynamic import of ParcelPanel component
import('@cw-parcelpanel/headless-react')
.then((module) => {
setComponent(() => module.TrackingWidget)
})
.catch((err) => {
console.error('Failed to load TrackingWidget:', err)
props.onError?.(err)
})
}, [mounted, props])
// Return null during SSR
if (!mounted) {
return null
}
// Loading state
if (!Component) {
return (
<div className="text-gray-500 text-sm">
Loading order tracking...
</div>
)
}
// Render component
return <Component {...props} />
}
/**
* Extract numeric ID from Shopify GID
* Example: gid://shopify/Product/123456 -> 123456
*/
function extractShopifyId(gid: string): string {
if (!gid) return ''
// If already numeric, return as-is
if (/^\d+$/.test(gid)) {
return gid
}
// Extract ID from gid://shopify/Product/123456 format
const match = gid.match(/gid:\/\/shopify\/Product\/(\d+)/)
return match ? match[1] : gid
}
/**
* Simple client-only check hook
*/
function useClientOnly() {
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return mounted
}
/**
* ParcelPanel EDD Component - Client-side only
* Usage:
* <ParcelPanelEDD shopDomain="..." productId="..." locale="..." customer={{...}} />
*/
export function ParcelPanelEDD(props: ParcelPanelEDDProps) {
const mounted = useClientOnly()
const [Component, setComponent] = useState<any>(null)
useEffect(() => {
if (!mounted) return
// Dynamic import of ParcelPanel component
import('@cw-parcelpanel/headless-react')
.then((module) => {
setComponent(() => module.ParcelPanelEDD)
})
.catch((err) => {
console.error('Failed to load ParcelPanelEDD:', err)
props.onError?.(err)
})
}, [mounted, props])
// Return null during SSR
if (!mounted) {
return null
}
// Loading state
if (!Component) {
return (
<div className="text-gray-500 text-sm">
Loading delivery information...
</div>
)
}
// Render component
const numericProductId = extractShopifyId(props.productId)
return <Component {...props} productId={numericProductId} />
}2. Configure Content Security Policy (CSP)
⚠️ Important: Hydrogen Storefront CSP Restrictions
Hydrogen Storefront Special Requirements:
Shopify Hydrogen framework enables strict Content Security Policy (CSP) by default for:
- Security Protection - Prevent XSS attacks and malicious script injection
- Performance Optimization - Control resource loading for better performance
- Mobile Optimization - Ensure security and performance on mobile devices
Default CSP Restrictions:
default-src 'self' https://cdn.shopify.com https://shopify.com http://localhost:*This means by default, Hydrogen only allows:
- ✅ Same-origin resources (
'self') - ✅ Official Shopify CDN
- ✅ Local development server
ParcelPanel Component Resource Requirements:
ParcelPanel components need access to the following external resources at runtime:
- Load external scripts - Get tracking logic from
pp-proxy.parcelpanel.com - Display image resources - Carrier logos, ParcelPanel logos, etc.
- Load web fonts - Ensure component UI status icons display correctly
- Establish network connections - Real-time delivery information and tracking data
- Embed iframes - Tracking features may need to load external pages (Google Maps)
Consequences of Not Updating CSP:
If you don't update CSP configuration, Hydrogen's strict security policy will block these resource loads, causing browser console errors:
❌ Refused to load the script 'https://pp-proxy.parcelpanel.com/...'
because it violates the following Content Security Policy directive
❌ Refused to load the image 'https://cdn.parcelpanel.com/...'
because it violates the following Content Security Policy directive
❌ Refused to connect to 'https://pp-proxy.parcelpanel.com/api/...'
because it violates the following Content Security Policy directive
Result:
- ❌ Components cannot display properly
- ❌ Delivery information retrieval fails
- ❌ Console shows numerous CSP errors
- ❌ Users see blank or broken component interface
⚠️ This is a REQUIRED step for Hydrogen projects, not optional!
CSP Configuration Steps
Update CSP configuration in app/entry.server.tsx:
import type {AppLoadContext} from '@shopify/remix-oxygen';
import {ServerRouter} from 'react-router';
import {isbot} from 'isbot';
import {renderToReadableStream} from 'react-dom/server';
import {createContentSecurityPolicy} from '@shopify/hydrogen';
import type {EntryContext} from 'react-router';
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
context: AppLoadContext,
) {
const {nonce, header, NonceProvider} = createContentSecurityPolicy({
shop: {
checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN,
storeDomain: context.env.PUBLIC_STORE_DOMAIN,
},
// Allow network connections - API calls and delivery data
connectSrc: [
"'self'",
'https://pp-proxy.parcelpanel.com', // ParcelPanel main API
'https://cdn.parcelpanel.com', // ParcelPanel CDN
'https://*.shopify.com', // Shopify services
],
// Allow script loading - ParcelPanel core functionality
scriptSrc: [
"'self'",
"'unsafe-inline'", // Inline script support
'https://pp-proxy.parcelpanel.com', // ParcelPanel scripts
'https://cdn.parcelpanel.com', // ParcelPanel CDN scripts
'https://*.shopify.com', // Shopify scripts
],
// Allow image loading - Carrier icons, status images, etc.
imgSrc: [
"'self'",
'data:', // Base64 image support
'https:', // All HTTPS images
'https://pp-proxy.parcelpanel.com', // ParcelPanel images
'https://cdn.parcelpanel.com', // ParcelPanel CDN images
'https://*.shopifycdn.net', // Shopify CDN images
],
// Allow font loading - Component interface fonts
fontSrc: [
"'self'",
'data:', // Base64 font support
'https://pp-proxy.parcelpanel.com', // ParcelPanel fonts
'https://cdn.parcelpanel.com', // ParcelPanel CDN fonts
'https://*.shopifycdn.net', // Shopify fonts
],
// Allow iframe embedding - Certain tracking features
frameSrc: [
"'self'",
'https://*.google.com', // Google Maps and services
'https://pp-proxy.parcelpanel.com', // ParcelPanel iframes
'https://cdn.parcelpanel.com', // ParcelPanel CDN
],
});
// ... rest of the code remains unchanged
const body = await renderToReadableStream(
<NonceProvider>
<ServerRouter
context={reactRouterContext}
url={request.url}
nonce={nonce}
/>
</NonceProvider>,
{
nonce,
signal: request.signal,
onError(error) {
console.error(error);
responseStatusCode = 500;
},
},
);
if (isbot(request.headers.get('user-agent'))) {
await body.allReady;
}
responseHeaders.set('Content-Type', 'text/html');
responseHeaders.set('Content-Security-Policy', header);
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}Configuration Points Explained
| CSP Directive | Purpose | ParcelPanel Requirements |
|---|---|---|
connectSrc | Control network requests | Get delivery info, API calls |
scriptSrc | Control script loading | Load tracking logic, component features |
imgSrc | Control image loading | Carrier icons, status icons |
fontSrc | Control font loading | Component interface font rendering |
frameSrc | Control iframe | Map display, external tracking pages |
Security Notes
These CSP configurations are required but still maintain security:
- ✅ Specific domain whitelist - Only allow ParcelPanel and official Shopify domains
- ✅ Protocol restrictions - Primarily HTTPS, HTTP only for development
- ✅ The Least privilege principle - Only add minimum permissions needed for ParcelPanel
- ✅ Shopify ecosystem compatibility - Fully compatible with Shopify security standards
3. Configure Vite SSR External
In vite.config.ts:
import {defineConfig} from 'vite';
import {hydrogen} from '@shopify/hydrogen/vite';
import {oxygen} from '@shopify/mini-oxygen/vite';
import {reactRouter} from '@react-router/dev/vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
tailwindcss(),
hydrogen(),
oxygen(),
reactRouter(),
tsconfigPaths(),
],
build: {
// Allow a strict Content-Security-Policy
// withtout inlining assets as base64:
assetsInlineLimit: 0,
},
ssr: {
// Exclude ParcelPanel from server-side rendering
external: ['@cw-parcelpanel/headless-react'],
optimizeDeps: {
include: [],
},
},
});4. Use TrackingWidget Component in Tracking Pages
Create app/routes/($locale).ordertrack.tsx:
import {type MetaFunction} from 'react-router';
import { TrackingWidget } from '~/components/ParcelPanelComponents';
export const meta: MetaFunction = () => {
return [{title: `Hydrogen | Order Track`}];
};
export default function OrderTrack() {
return (
<div className="order-track">
<TrackingWidget
shopDomain="your-shop.myshopify.com"
locale="en"
className="tracking-container"
onLoaded={() => console.log('Tracking widget loaded')}
onError={(error) => console.error('Tracking error:', error)}
/>
</div>
);
}
Benefits of this approach:
- ✅ Dynamic Addition - Order Track will appear regardless of Shopify admin menu configuration
- ✅ Avoid Duplicates - Automatically checks to prevent duplicate menu items
- ✅ More Flexible - Not dependent on FALLBACK_HEADER_MENU, works with all menu configurations
5. Use EDD Component in Product Pages
In app/routes/($locale).products.$handle.tsx:
import { ParcelPanelEDD } from '~/components/ParcelPanelComponents';
export default function Product() {
const {product} = useLoaderData<typeof loader>();
// ... other code
return (
<div className="product">
{/* Your product content */}
{/* ParcelPanel EDD Widget */}
<ParcelPanelEDD
shopDomain="your-shop.myshopify.com"
productId={product.id}
locale="en"
customer={{ // For automatic user location detection (optional)
country_code: "US", // From user location or checkout info
province_code: "CA"
}}
onLoaded={(sdk) => console.log('EDD widget loaded:', sdk)}
onError={(error) => console.error('EDD error:', error)}
/>
</div>
);
}Pure Client-Side Rendering Projects (CSR)
If your project is purely client-side rendered (no SSR), you can use components directly without wrappers:
CSP Configuration Differences
Important Distinction:
| Project Type | CSP Configuration Method | Required? |
|---|---|---|
| Hydrogen Storefront | Configure in app/entry.server.tsx | ✅ REQUIRED - Strict default CSP |
| Next.js (App Router) | next.config.js or middleware | ⚠️ Might need - Depends on config |
| Next.js (Pages Router) | next.config.js or custom headers | ⚠️ Might need - Depends on config |
| Create React App | No built-in CSP | ❌ Usually not needed |
| Vite React | No built-in CSP | ❌ Usually not needed |
| Remix | Similar config in entry.server.tsx | ⚠️ Might need - Depends on config |
If your project doesn't have CSP enabled, you can skip the CSP configuration steps.
Next.js Pages Router (CSR)
// pages/products/[id].tsx
import { ParcelPanelEDD } from '@cw-parcelpanel/headless-react';
export default function ProductPage({ product }) {
return (
<div>
<h1>{product.title}</h1>
<ParcelPanelEDD
shopDomain="your-shop.myshopify.com"
productId={product.id}
locale="en"
customer={{ // For automatic user location detection (optional)
country_code: "US", // From user location or checkout info
province_code: "CA"
}}
/>
</div>
);
}React SPA (Vite/CRA)
// src/components/ProductPage.tsx
import { ParcelPanelEDD } from '@cw-parcelpanel/headless-react';
function ProductPage({ product }) {
return (
<div>
<h1>{product.title}</h1>
<ParcelPanelEDD
shopDomain="your-shop.myshopify.com"
productId={product.id}
locale="en"
/>
</div>
);
}Multi-language Support
Components support multiple languages:
// English
<ParcelPanelEDD locale="en" />
// Simplified Chinese
<ParcelPanelEDD locale="zh-CN" />
// Traditional Chinese
<ParcelPanelEDD locale="zh-TW" />
// Japanese
<ParcelPanelEDD locale="ja" />
// Spanish
<ParcelPanelEDD locale="es" />
// French
<ParcelPanelEDD locale="fr" />Custom Styling
CSS Class Customization
<ParcelPanelEDD
className="my-custom-edd-widget"
shopDomain="your-shop.myshopify.com"
productId={product.id}
locale="en"
customer={{ // For automatic user location detection (optional)
country_code: "US", // From user location or checkout info
province_code: "CA"
}}
/>/* In your CSS file */
.my-custom-edd-widget {
border: 1px solid #e1e5e9;
border-radius: 8px;
padding: 16px;
margin: 20px 0;
}Checklist
Hydrogen Storefront Specific Checklist
Before deployment, MUST confirm:
- Installed
@cw-parcelpanel/headless-react Created app/components/ParcelPanelComponents.tsx(with client-only components)Configured vite.config.ts SSR external exclusion(prevent server inclusion)Updated CSP configuration in app/entry.server.tsx(Required step!)- Restarted Hydrogen development server for configuration to take effect
shopDomainconfigured correctly (format:your-shop.myshopify.com)- Product ID format correct (automatically converts Shopify GID)
- Tested loading states and error handling
- Browser console shows no CSP errors
Other Framework Checklist
For non-Hydrogen projects:
- Installed
@cw-parcelpanel/headless-react - If using SSR, created wrapper components
- If project has CSP enabled, configured appropriate policies
shopDomainconfigured correctly- Tested component functionality
⚠️ Important Reminder
Hydrogen Storefront users MUST complete CSP configuration, otherwise components won't work!
This is not an optional step, but a security requirement of the Hydrogen framework.
Technical Support
If you encounter issues, please provide the following information:
- Project Environment
- ParcelPanel Configuration
- Error Information
Related Resources
Version Updates
Regularly update to the latest version:
npm update @cw-parcelpanel/headless-reactVersion Compatibility
Current Version: @cw-parcelpanel/[email protected]+This guide applies to the latest version of ParcelPanel Headless React components. If you're using an older version, we recommend upgrading to the latest version for the best experience and security.
Version Upgrade
# Check current version
npm list @cw-parcelpanel/headless-react
# Update to latest version
npm update @cw-parcelpanel/headless-react
# Or reinstall latest version
npm install @cw-parcelpanel/headless-react@latestCompatibility Notes
- ✅ React 16.8+ - Hooks support
- ✅ TypeScript 4.0+ - Full type support
- ✅ Node.js 16+ - Modern Node.js environment
- ✅ Shopify Hydrogen 2024+ - Latest Hydrogen framework
- ✅ Next.js 13+ - App Router and Pages Router
- ✅ Remix 2.0+ - Modern Remix framework
Note: This guide is continuously updated to keep up with the latest versions. If you have questions, please refer to the Official NPM Page for the latest information.