Skip to content
Learni
View all tutorials
Analytics & Tracking

How to Set Up Retargeting with GTM and GA4 in 2026

Lire en français

Introduction

Retargeting, or remarketing, shows personalized ads to visitors who interacted with your site but didn't convert. In 2026, with third-party cookies deprecated and Privacy Sandbox rising, robust retargeting requires a hybrid approach: client-side tracking via Google Tag Manager (GTM), enriched GA4 events, and audiences synced to Google Ads.

This intermediate tutorial walks you through creating a functional Next.js project, integrating GTM for advanced e-commerce events (like view_item or purchase), configuring segmented audiences, and optimizing for consent mode v2. You'll generate behavior-based audience lists to boost conversions by 20-50% based on industry benchmarks. By the end, you'll have a production-ready, scalable, GDPR/CCPA-compliant setup. Get your Google accounts ready: let's dive in.

Prerequisites

  • Node.js 20+ installed
  • Basic knowledge of Next.js 15 (App Router) and TypeScript
  • Google accounts: Analytics 4 (web data stream property), Tag Manager (new Web container), Google Ads (linked account)
  • A test domain or localhost for deployment
  • Tools: VS Code editor, terminal

Initialize the Next.js Project

terminal
npx create-next-app@latest retargeting-demo --typescript --tailwind --eslint --app --src-dir --import-alias="@/*"
cd retargeting-demo
npm install
npm run dev

This command creates a complete Next.js 15 project with TypeScript, Tailwind, and App Router. It sets up all the necessary boilerplate for a demo e-commerce site. Run npm run dev to test at http://localhost:3000; skip empty templates to avoid reinventing the wheel.

Create and Configure Your GTM Container

  1. Go to tagmanager.google.com and create a new Web container named 'Retargeting Demo'.
  2. Note the GTM ID (format GTM-XXXXXX) in Settings > Container.
  3. Publish an empty version (Submit > Publish).
  4. In GA4, create a web data stream if needed, and note the Measurement ID (G-YYYYYY).
These steps prepare the infrastructure: GTM handles tags without cluttering your source code, while GA4 collects data for audiences.

Add the GTM Script to the Layout

src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Retargeting Demo',
  description: 'Demo site for GTM/GA4 retargeting',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
              new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
              j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
              'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
              })(window,document,'script','dataLayer','GTM-XXXXXX');
            `,
          }}
        />
      </head>
      <body className={inter.className}>{children}
        <noscript
          dangerouslySetInnerHTML={{
            __html: `
              <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
              height="0" width="0" style="display:none;visibility:hidden"></iframe>
            `,
          }}
        />
      </body>
    </html>
  );
}

Replace GTM-XXXXXX with your real ID. This code injects the main GTM script (async) and noscript fallback into the . Use dangerouslySetInnerHTML for Next.js SSR; it's safe since it's static. Test: dataLayer is initialized, and GTM Preview mode detects the container.

Configure the GA4 Tag in GTM

In the GTM interface (Preview mode enabled):

  • Add a tag > GA4 > Configuration: paste your Measurement ID (G-YYYYYY).
  • Trigger: Initialization - All Pages.
  • Add a second GA4 Event tag: name 'page_view', trigger All Pages.

Publish. Check GA4 Realtime Reports: hits are arriving. This sets up basic tracking for 'All Sessions' audiences.

Push a view_item_list Event

src/app/page.tsx
import { useEffect } from 'react';

export default function Home() {
  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'view_item_list',
      ecommerce: {
        items: [
          {
            item_id: 'SKU_12345',
            item_name: 'Nike Air Shoes',
            item_category: 'Shoes',
            item_category2: 'Sport',
            price: 89.99,
            quantity: 1,
          },
          {
            item_id: 'SKU_67890',
            item_name: 'Adidas Jacket',
            item_category: 'Clothing',
            price: 129.99,
            quantity: 1,
          },
        ],
      },
    });
  }, []);

  return (
    <main className="flex min-h-screen flex-col items-center justify-center p-24">
      <h1 className="text-4xl font-bold">Retargeting Demo</h1>
      <p>Product list loaded - GA4 event sent!</p>
    </main>
  );
}

This useEffect hook pushes a standard GA4 e-commerce event via dataLayer on load. The 'ecommerce.items' params enrich data for segmented audiences (e.g., catalog visitors). Check in GTM Preview: GA4 tag fires on 'view_item_list'. Avoid multiple pushes with empty deps [].

Create Advanced Triggers and Tags in GTM

  1. dataLayer Variable: Add 'ecommerce' (object).
  2. Trigger: Custom > JS, condition {{DL - ecommerce}}.
  3. GA4 Event Tag: name '{{Event}}', params from DL (ecommerce).
  4. Test on home page: event fires.
This enables dynamic tracking without hardcoding in JS.

Implement the purchase Event

src/app/purchase/page.tsx
'use client';
import { useState } from 'react';

export default function PurchasePage() {
  const [purchased, setPurchased] = useState(false);

  const handlePurchase = () => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
      event: 'purchase',
      ecommerce: {
        transaction_id: 'T12345',
        value: 219.98,
        tax: 19.98,
        shipping: 10.00,
        currency: 'EUR',
        coupon: 'OFFRE10',
        items: [
          {
            item_id: 'SKU_12345',
            item_name: 'Nike Air Shoes',
            price: 89.99,
            quantity: 1,
          },
          {
            item_id: 'SKU_67890',
            item_name: 'Adidas Jacket',
            price: 129.99,
            quantity: 1,
          },
        ],
      },
    });
    setPurchased(true);
  };

  return (
    <main className="p-24">
      <h1>Simulated Purchase</h1>
      <button onClick={handlePurchase} className="bg-blue-500 text-white p-4 rounded">
        Confirm Purchase
      </button>
      {purchased && <p>Purchase event sent to GA4!</p>
      }
    </main>
  );
}

Dedicated page simulates a checkout: click pushes 'purchase' with full transaction details. Use for 'Recent Buyers' audience. State handles UI feedback. Navigate to /purchase, test in GTM/GA4: ecomm data validates the retargeting funnel.

Enable Google Consent Mode v2

In 2026, consent mode is required for >70% of GA4 signals. In GTM: Consent Overview tag > ad_storage, analytics_storage. Script before GTM.

Add Consent Mode to the Head

src/app/layout.tsx-update
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
  title: 'Retargeting Demo',
  description: 'Demo site for GTM/GA4 retargeting',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        <script
          dangerouslySetInnerHTML={{
            __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('consent', 'default', {
                'ad_storage': 'denied',
                'analytics_storage': 'denied',
                'ad_user_data': 'denied',
                'ad_personalization': 'denied'
              });
            `,
          }}
          id="gtag-init"
        />
        <script
          dangerouslySetInnerHTML={{
            __html: `
              (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
              new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
              j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
              'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
              })(window,document,'script','dataLayer','GTM-XXXXXX');
            `,
          }}
        />
      </head>
      <body className={inter.className}>{children}
        <noscript
          dangerouslySetInnerHTML={{
            __html: `
              <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXX"
              height="0" width="0" style="display:none;visibility:hidden"></iframe>
            `,
          }}
        />
      </body>
    </html>
  );
}

Adds consent default 'denied' before GTM, compliant with v2. Integrate a CMP (e.g., CookieYes) to update via gtag('consent', 'update', {...}). This preserves modeled signals in GA4 for cookieless retargeting.

Create Audiences and Link to Google Ads

  1. GA4 > Admin > Audiences: New > 'Users who triggered view_item_list' (30 days), 'Buyers' (purchase, 7 days).
  2. Google Ads > Tools > Audiences: Authorize GA4 link.
  3. Create Display/Performance Max campaign, target audiences (e.g., retarget cart abandoners).
Wait 24-48h for population (min 100 users).

Best Practices

  • Always prioritize server-side tagging for >50% mobile traffic (via Cloudflare/Next.js API).
  • Limit dataLayer pushes to <10/page for performance (useEffect + debounce).
  • Test with GTM Preview + GA4 DebugView before prod.
  • Segment fine-grained audiences (e.g., high-value >$200) for 3x ROI.
  • Integrate OneTrust CMP for granular consent, boosting ad fill rate by 15%.

Common Mistakes to Avoid

  • Forgetting window.dataLayer || []: JS crash if undefined.
  • Pushing events without structured ecommerce: empty GA4 audiences.
  • Ignoring consent mode: >80% 'not set' signals, weak retargeting.
  • Not linking GA4-Ads: audiences invisible in Ads manager.

Next Steps

Dive deeper with our advanced GA4 courses at Learni. Resources: GTM Events Docs, GA4 Audiences, Privacy Sandbox. Migrate to server-side GTM for full cookieless in 2026.