Files
manoon-headless/mautic-abandoned-cart.md
Unchained 7b94537670 feat(saleor): Phase 1 - GraphQL Client Setup
- 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
2026-03-21 12:36:21 +02:00

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

  1. Go to https://mautic.nodecrew.me
  2. Settings → Custom Fields
  3. 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)

  1. Segments → New
  2. Name: "Abandoned Cart - 1 Hour"
  3. Filters:
    • Cart Abandoned = true
    • Cart Abandoned At > 1 hour ago
    • Cart Recovered = false
    • Email = not empty

Segment 2: Abandoned Cart (24 hours)

  1. Segments → New
  2. Name: "Abandoned Cart - 24 Hours"
  3. 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

  1. Campaigns → New
  2. Name: "Abandoned Cart Recovery"
  3. 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

  1. Add item to cart on storefront
  2. Enter email at checkout
  3. Close browser (don't complete purchase)
  4. Wait 1 hour
  5. Check Mautic → Contact should have cart_abandoned = true
  6. 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.