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
npx create-next-app@latest retargeting-demo --typescript --tailwind --eslint --app --src-dir --import-alias="@/*"
cd retargeting-demo
npm install
npm run devThis 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
- Go to tagmanager.google.com and create a new Web container named 'Retargeting Demo'.
- Note the GTM ID (format GTM-XXXXXX) in Settings > Container.
- Publish an empty version (Submit > Publish).
- In GA4, create a web data stream if needed, and note the Measurement ID (G-YYYYYY).
Add the GTM Script to the Layout
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
. UsedangerouslySetInnerHTML 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
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
- dataLayer Variable: Add 'ecommerce' (object).
- Trigger: Custom > JS, condition
{{DL - ecommerce}}. - GA4 Event Tag: name '{{Event}}', params from DL (ecommerce).
- Test on home page: event fires.
Implement the purchase Event
'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
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
- GA4 > Admin > Audiences: New > 'Users who triggered view_item_list' (30 days), 'Buyers' (purchase, 7 days).
- Google Ads > Tools > Audiences: Authorize GA4 link.
- Create Display/Performance Max campaign, target audiences (e.g., retarget cart abandoners).
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.