- Add Apollo Client for Saleor GraphQL API - Create GraphQL fragments (Product, Variant, Checkout) - Create GraphQL queries (Products, Checkout) - Create GraphQL mutations (Checkout operations) - Add TypeScript types for Saleor entities - Add product helper functions - Install @apollo/client and graphql dependencies Part of WordPress/WooCommerce → Saleor migration
12 KiB
Mautic Abandoned Cart Recovery Setup
Overview
Use your existing Mautic instance for abandoned cart recovery instead of paying for Klaviyo.
Mautic URL: https://mautic.nodecrew.me
How It Works
1. User adds item to cart
↓
2. Storefront sends event to Mautic (via API or tracking pixel)
↓
3. Mautic creates/updates contact with cart data
↓
4. Campaign waits 1 hour
↓
5. If no purchase → Send abandoned cart email
↓
6. User clicks email → Cart restored → Convert!
Step 1: Set Up Mautic Tracking
Option A: Mautic Tracking Pixel (JavaScript)
Add to your Next.js storefront:
// lib/mautic.ts
export function trackAddToCart(product: any, quantity: number) {
if (typeof window !== 'undefined' && (window as any).mt) {
(window as any).mt('send', 'pageview', {
page_title: `Added to Cart: ${product.name}`,
page_url: window.location.href,
product_name: product.name,
product_sku: product.variants[0]?.sku,
product_price: product.variants[0]?.channelListings[0]?.price?.amount,
quantity: quantity,
event: 'add_to_cart'
});
}
}
export function trackCheckoutStarted(checkout: any) {
if (typeof window !== 'undefined' && (window as any).mt) {
(window as any).mt('send', 'pageview', {
page_title: 'Checkout Started',
page_url: window.location.href,
checkout_value: checkout.totalPrice?.amount,
checkout_id: checkout.id,
event: 'checkout_started'
});
}
}
export function trackOrderCompleted(order: any) {
if (typeof window !== 'undefined' && (window as any).mt) {
(window as any).mt('send', 'pageview', {
page_title: 'Order Completed',
page_url: window.location.href,
order_total: order.total.gross.amount,
order_id: order.id,
event: 'purchase_completed'
});
}
}
// pages/_app.tsx or layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<head>
{/* Mautic Tracking */}
<Script
id="mautic-tracking"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
(function(w,d,t,u,n,a,m){
w['MauticTrackingObject']=n;
w[n]=w[n]||function(){(w[n].q=w[n].q||[]).push(arguments)},a=d.createElement(t),
m=d.getElementsByTagName(t)[0];a.async=1;a.src=u;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://mautic.nodecrew.me/mtc.js','mt');
mt('send', 'pageview');
`
}}
/>
</head>
<body>{children}</body>
</html>
);
}
Option B: Direct Mautic API Integration
More reliable for e-commerce events:
// lib/mautic-api.ts
const MAUTIC_URL = 'https://mautic.nodecrew.me';
const MAUTIC_USERNAME = process.env.MAUTIC_API_USER;
const MAUTIC_PASSWORD = process.env.MAUTIC_API_PASS;
export async function createOrUpdateContact(email: string, data: any) {
const response = await fetch(`${MAUTIC_URL}/api/contacts/new`, {
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(`${MAUTIC_USERNAME}:${MAUTIC_PASSWORD}`).toString('base64')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
firstname: data.firstName,
lastname: data.lastName,
phone: data.phone,
// Custom fields for cart
cart_items: JSON.stringify(data.cartItems),
cart_value: data.cartValue,
cart_abandoned: true,
cart_abandoned_at: new Date().toISOString(),
last_product_added: data.lastProductName,
}),
});
return response.json();
}
export async function trackCartAbandoned(email: string, checkout: any) {
return createOrUpdateContact(email, {
cartItems: checkout.lines.map((line: any) => ({
name: line.variant.name,
quantity: line.quantity,
price: line.totalPrice.gross.amount,
})),
cartValue: checkout.totalPrice.gross.amount,
lastProductName: checkout.lines[0]?.variant.name,
});
}
export async function markCartRecovered(email: string) {
const response = await fetch(`${MAUTIC_URL}/api/contacts/edit`, {
method: 'PATCH',
headers: {
'Authorization': `Basic ${Buffer.from(`${MAUTIC_USERNAME}:${MAUTIC_PASSWORD}`).toString('base64')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
cart_abandoned: false,
cart_recovered: true,
cart_recovered_at: new Date().toISOString(),
}),
});
return response.json();
}
Step 2: Create Custom Fields in Mautic
- Go to https://mautic.nodecrew.me
- Settings → Custom Fields
- Create these fields:
| Field Label | Alias | Data Type | Default Value |
|---|---|---|---|
| Cart Items | cart_items | Text | |
| Cart Value | cart_value | Number | 0 |
| Cart Abandoned | cart_abandoned | Boolean | false |
| Cart Abandoned At | cart_abandoned_at | Date/Time | |
| Last Product Added | last_product_added | Text | |
| Cart Recovered | cart_recovered | Boolean | false |
Step 3: Create Segments
Segment 1: Abandoned Cart (1 hour)
- Segments → New
- Name: "Abandoned Cart - 1 Hour"
- Filters:
- Cart Abandoned = true
- Cart Abandoned At > 1 hour ago
- Cart Recovered = false
- Email = not empty
Segment 2: Abandoned Cart (24 hours)
- Segments → New
- Name: "Abandoned Cart - 24 Hours"
- Filters:
- Cart Abandoned = true
- Cart Abandoned At > 1 day ago
- Cart Recovered = false
- Email = not empty
Step 4: Create Email Templates
Email 1: First Reminder (1 hour)
Subject: Zaboravili ste nešto u korpi / You left something in your cart
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<!-- Serbian Version -->
<h2>Zdravo {contactfield=firstname},</h2>
<p>Primijetili smo da ste ostavili artikle u korpi:</p>
<!-- Cart Items (you'd dynamically insert these) -->
<div style="border: 1px solid #ddd; padding: 15px; margin: 15px 0;">
<p><strong>Poslednji proizvod:</strong> {contactfield=last_product_added}</p>
<p><strong>Vrednost korpe:</strong> {contactfield=cart_value} USD</p>
</div>
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}"
style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
Završite kupovinu
</a>
<hr>
<!-- English Version -->
<h2>Hello {contactfield=firstname},</h2>
<p>We noticed you left items in your cart:</p>
<div style="border: 1px solid #ddd; padding: 15px; margin: 15px 0;">
<p><strong>Last product:</strong> {contactfield=last_product_added}</p>
<p><strong>Cart value:</strong> {contactfield=cart_value} USD</p>
</div>
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}"
style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
Complete Purchase
</a>
</body>
</html>
Email 2: Second Reminder (24 hours) - With Discount
Subject: Još uvijek čekamo! / Still waiting for you! (10% off)
<!DOCTYPE html>
<html>
<body>
<h2>Hej {contactfield=firstname},</h2>
<p>Vaša korpa još uvijek čeka! Dajemo vam <strong>10% popusta</strong> da završite kupovinu:</p>
<p>Koristite kod: <strong>COMEBACK10</strong></p>
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}&coupon=COMEBACK10"
style="background: #28a745; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
Završite kupovinu sa 10% popusta
</a>
<hr>
<h2>Hey {contactfield=firstname},</h2>
<p>Your cart is still waiting! Here's <strong>10% off</strong> to complete your purchase:</p>
<p>Use code: <strong>COMEBACK10</strong></p>
<a href="https://dev.manoonoils.com/cart-recovery?email={contactfield=email}&coupon=COMEBACK10"
style="background: #28a745; color: white; padding: 12px 24px; text-decoration: none; display: inline-block;">
Complete Purchase with 10% Off
</a>
</body>
</html>
Step 5: Create Campaign
Campaign Workflow
- Campaigns → New
- Name: "Abandoned Cart Recovery"
- Description: "Recover abandoned carts with 2-email sequence"
Campaign Canvas:
[Contact enters campaign]
↓
[Decision: Cart Abandoned?]
↓ Yes
[Wait: 1 hour]
↓
[Send Email: First Reminder]
↓
[Wait: 23 hours]
↓
[Decision: Cart Recovered?]
↓ No
[Send Email: Second Reminder + 10% off]
↓
[Wait: 3 days]
↓
[Decision: Cart Recovered?]
↓ No
[Remove from campaign]
Campaign Settings
Entry Conditions:
- Contact added to segment "Abandoned Cart - 1 Hour"
Exit Conditions:
- Cart Recovered = true
- Order Completed event triggered
Step 6: Cart Recovery Page
Create a recovery page in Next.js:
// pages/cart-recovery.tsx
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { saleorClient } from '@/lib/saleor/client';
import { gql } from '@apollo/client';
import { markCartRecovered } from '@/lib/mautic-api';
const GET_CHECKOUT_BY_EMAIL = gql`
query GetCheckoutByEmail($email: String!) {
checkouts(first: 1, filter: {customer: $email}) {
edges {
node {
id
token
lines {
id
quantity
variant {
id
name
product {
name
}
}
}
}
}
}
}
`;
export default function CartRecoveryPage() {
const router = useRouter();
const { email, coupon } = router.query;
useEffect(() => {
if (email) {
// Mark cart as recovered in Mautic
markCartRecovered(email as string);
// Redirect to checkout with recovered cart
// You'll need to implement checkout restoration logic
router.push(`/checkout?email=${email}${coupon ? `&coupon=${coupon}` : ''}`);
}
}, [email, coupon, router]);
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
<p>Restoring your cart...</p>
</div>
);
}
Step 7: Storefront Integration
Add tracking to your add-to-cart and checkout flows:
// components/AddToCartButton.tsx
import { trackAddToCart } from '@/lib/mautic';
import { trackCartAbandoned } from '@/lib/mautic-api';
export function AddToCartButton({ product, variant, quantity }) {
const handleAddToCart = async () => {
// Add to Saleor cart
await addToCart(variant.id, quantity);
// Track in Mautic
trackAddToCart(product, quantity);
};
return <button onClick={handleAddToCart}>Add to Cart</button>;
}
// components/CheckoutForm.tsx
import { trackCheckoutStarted } from '@/lib/mautic';
export function CheckoutForm({ checkout, email }) {
useEffect(() => {
if (checkout && email) {
trackCheckoutStarted(checkout);
}
}, [checkout, email]);
// Track abandonment when user leaves
useEffect(() => {
const handleBeforeUnload = () => {
if (!orderCompleted && checkout) {
trackCartAbandoned(email, checkout);
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [checkout, email, orderCompleted]);
return <form>...</form>;
}
Testing
- Add item to cart on storefront
- Enter email at checkout
- Close browser (don't complete purchase)
- Wait 1 hour
- Check Mautic → Contact should have cart_abandoned = true
- Check email → Should receive first reminder
Monitoring
Track campaign performance in Mautic:
- Emails Sent
- Open Rate
- Click Rate
- Conversion Rate (cart recovered)
- Revenue Generated
Cost Comparison
| Solution | Monthly Cost | Setup Time |
|---|---|---|
| Mautic (existing) | FREE | 2-3 days |
| Klaviyo | $20-50+ | 1 day |
| Custom Build | FREE | 2-4 weeks |
Summary
✅ Mautic CAN do abandoned cart recovery ✅ Use your existing instance = FREE ⚠️ Requires custom integration work ⚠️ Email templates need manual setup
Recommendation: Since you already pay for Mautic hosting, use it for abandoned cart instead of paying for Klaviyo. The setup is moderate complexity but saves $20-50/month.