DEV Community

Cover image for Implementing Google Analytics in Consent Mode with a Cookie Banner for Next.js with TS.
abdessamad idboussadel
abdessamad idboussadel

Posted on

Implementing Google Analytics in Consent Mode with a Cookie Banner for Next.js with TS.

In this article, I will show you how to set up Google Analytics 4 Consent Mode in your NextJs app in both appand pages router and with TypeScript.Consent mode asks users if it's okay to save cookies in their browser. Basic Google Analytics still works even if they say no, however, additional data will be added once consent is granted.

Before we start we need to install 2 packages :

npm install @types/gtag.js --save-dev
Enter fullscreen mode Exit fullscreen mode

Otherwise, some variables would be unknown and undefined. This This adds the type definitions for Google's gtag.js script which is required only if you're using TypeScript.

npm install client-only
Enter fullscreen mode Exit fullscreen mode

The latest feature in Next.js 13 enables us to specify files that should only run on the user's browser (client-side). We'll utilize this functionality when dealing with local storage to manage and store cookie consent information.

Setup Your Google Analytics

Create your account here :

google analytics create account page

Fill out the form then click on web :

Google Analytics website image

Fill out the form :
Google Analytics website image

Now you will see this page :

Google Analytics website image

we will use the Measurement ID in what comes next.

Adding code to NextJs :

First, we will build the GoogleAnalytics component which will store the basic gtag.js code which interacts with Google Analytics,
with "use client" to ensure that this will run on the client side.

In the second script, there's a recently introduced "consent" section that sets the analytics_storage cookies to "denied" by default. This aligns with Google Analytics Consent Mode, allowing us to later update this consent status once the user agrees to accept cookies.

'use client';
import Script from 'next/script'

export default function GoogleAnalytics({GA_MEASUREMENT_ID} : {GA_MEASUREMENT_ID : string}){
    return (
        <>
            <Script strategy="afterInteractive" 
                src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}/>
            <Script id='google-analytics' strategy="afterInteractive"
                dangerouslySetInnerHTML={{
                __html: `
                window.dataLayer = window.dataLayer || [];
                function gtag(){dataLayer.push(arguments);}
                gtag('js', new Date());

                gtag('consent', 'default', {
                    'analytics_storage': 'denied'
                });

                gtag('config', '${GA_MEASUREMENT_ID}', {
                    page_path: window.location.pathname,
                });
                `,
                }}
            />
        </>
)}
Enter fullscreen mode Exit fullscreen mode

- For App router :
We then need to import & add this new Google Analytics component to our root layout which can be found in app/layout.tsx.

import GoogleAnalytics from '@/components/GoogleAnalytics';
Enter fullscreen mode Exit fullscreen mode

When adding to our root layout, make sure to replace GA_MEASUREMENT_ID with your Measurement ID from Google Analytics. You can store your Measurement ID in .env.local if you don't want to pass it directly.

<html lang="en">
    <GoogleAnalytics GA_MEASUREMENT_ID='G-xxxxxxxxxx'/>
    <body>{children}</body>
</html>
Enter fullscreen mode Exit fullscreen mode

- For Pages router :
The same thing only difference is you will import and add the same code to _app.tsx instead of the root layout.

Google Analytics for SPA :

At this stage, Google Analytics is configured on our site, but its functionality may not meet expectations.Next.js operates as a Single Page Application (SPA), where all pages are loaded in advance. This means that page changes are virtual and don't require a full load of a new page.

By default, Google Analytics may not track these virtual page changes in Next.js. To address this issue, refer to the solution below:

Initially, let's create a file named gtagHelper.ts in the lib folder. The pageView method in this file tells Google Analytics about a new page view. We use this method when switching between pages in our Single Page Application (SPA).

// lib/gtagHelper.ts

export const pageview = (GA_MEASUREMENT_ID : string, url : string) => {
    window.gtag("config", GA_MEASUREMENT_ID, {
        page_path: url,
    });
};
Enter fullscreen mode Exit fullscreen mode

We'll use the useEffect hook to call pageview whenever the URL of our app changes. This involves keeping an eye on changes in both the pathname and the search parameters of the URL.
This is the final GoogleAnalytics component :

"use client";
import Script from "next/script";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { pageview } from "@/lib/gtagHelper";

export default function GoogleAnalytics({
  GA_MEASUREMENT_ID,
}: {
  GA_MEASUREMENT_ID: string;
}) {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    const url = pathname + searchParams.toString();

    pageview(GA_MEASUREMENT_ID, url);
  }, [pathname, searchParams, GA_MEASUREMENT_ID]);
  return (
    <>
      <Script
        strategy="afterInteractive"
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
      />
      <Script
        id="google-analytics"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
                window.dataLayer = window.dataLayer || [];
                function gtag(){dataLayer.push(arguments);}
                gtag('js', new Date());

                gtag('consent', 'default', {
                    'analytics_storage': 'denied'
                });

                gtag('config', '${GA_MEASUREMENT_ID}', {
                    page_path: window.location.pathname,
                });
                `,
        }}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Cookie Banner Component :

We've successfully set up Google Analytics for Next.js, making it track page changes. However, it's not GDPR compliant right now because it uses cookie tracking by default. To address this, we must implement Google Analytics Consent Mode, enabling users to choose whether to allow cookie tracking.
Create your own custom cookie banner or use this one :

'use client';

import Link from 'next/link'

export default function CookieBanner(){
    return (
        <div className={`my-10 mx-auto max-w-max md:max-w-screen-sm
                        fixed bottom-0 left-0 right-0 
                        flex px-3 md:px-4 py-3 justify-between items-center flex-col sm:flex-row gap-4 z-50
                         bg-gray-700 rounded-lg shadow`}>

            <div className='text-center'>
                <Link href="/info/cookies"><p>We use <span className='font-bold text-sky-400'>cookies</span> on our site.</p></Link>
            </div>


            <div className='flex gap-2'>
                <button className='px-5 py-2 text-gray-300 rounded-md border-gray-900'>Decline</button>
                <button className='bg-gray-900 px-5 py-2 text-white rounded-lg'>Allow Cookies</button>
            </div>   
        </div>
    )}
Enter fullscreen mode Exit fullscreen mode

now we need to import the banner component :
- For the App router :

// app/layout.tsx
import CookieBanner from '@/components/CookieBanner';
Enter fullscreen mode Exit fullscreen mode
// app/layout.tsx
<html lang="en">
    <GoogleAnaytics GA_MEASUREMENT_ID='G-xxxxxxxxxx'/>
    <body>
        {children}
        <CookieBanner/>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

- For the Pages router :

// pages/_app.tsx
import CookieBanner from "@/components/CookieBanner";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <GoogleAnalytics GA_MEASUREMENT_ID="G-xxxxxxxxxxx" />
      <Component {...pageProps} />
      <CookieBanner />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

With our cookie banner, we'll ask users for permission the first time they come to the website. After that, we'll save their choice. So, the next time they visit, we'll remember their preferences.

To handle storing and retrieving values from local storage, let's create a file called storageHelper.ts also in the lib folder. This file utilizes client-only to ensure it runs exclusively on the client side, as localStorage is inaccessible on the server.

// lib/storageHelper.ts

import "client-only";

export function getLocalStorage(key: string, defaultValue:any){
    const stickyValue = localStorage.getItem(key);

    return (stickyValue !== null && stickyValue !== 'undefined')
        ? JSON.parse(stickyValue)
        : defaultValue;
}

export function setLocalStorage(key: string, value:any){
    localStorage.setItem(key, JSON.stringify(value));
}
Enter fullscreen mode Exit fullscreen mode

To implement these changes, we can enhance our cookie banner by incorporating the following modifications.

Initially, we establish a state named cookieConsent with a default value of false.

We use the first useEffect when the component loads to get the user's cookie consent from local storage. If it's not there, we set it to null and assign it to the cookieConsent state. This is about capturing user preferences.

The next useEffect is for updating Google Analytics based on these preferences. If cookieConsent is true, we allow consent; otherwise, we deny it. We also save these preferences in local storage, so we don't have to ask the user again.

For the finishing touches, we adjust the Allow and Decline buttons on our cookie banner to set the cookie consent to either true or false. The last step is to hide the cookie banner once the user accepts cookies, achieved by replacing the flex className.
This is the final CookieBanner component :

"use client";

import Link from "next/link";
import { getLocalStorage, setLocalStorage } from "@/lib/storageHelper";
import { useState, useEffect } from "react";

export default function CookieBanner() {
  const [cookieConsent, setCookieConsent] = useState(false);

  useEffect(() => {
    const storedCookieConsent = getLocalStorage("cookie_consent", null);

    setCookieConsent(storedCookieConsent);
  }, [setCookieConsent]);

  useEffect(() => {
    const newValue = cookieConsent ? "granted" : "denied";

    window.gtag("consent", "update", {
      analytics_storage: newValue,
    });

    setLocalStorage("cookie_consent", cookieConsent);
  }, [cookieConsent]);
  return (
    <div
      className={`my-10 mx-auto max-w-max md:max-w-screen-sm
                  fixed bottom-0 left-0 right-0 
                  flex px-3 md:px-4 py-3 justify-between items-center flex-col sm:flex-row gap-4  
                  bg-gray-700 rounded-lg shadow z-50
                  ${cookieConsent !== null ? "hidden" : "flex"}`}
    >
      <div className="text-cente text-white-200">
        <Link href="/info/cookies">
          <p>
            We use <span className="font-bold text-sky-400">cookies</span> on
            our site.
          </p>
        </Link>
      </div>

      <div className="flex gap-2">
        <button
          className="px-5 py-2 text-gray-300 rounded-md border-gray-900"
          onClick={() => setCookieConsent(false)}
        >
          Decline
        </button>
        <button
          className="bg-gray-900 px-5 py-2 text-white-200 rounded-lg"
          onClick={() => setCookieConsent(true)}
        >
          Allow Cookies
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Conclusion :

After setting up your Google Analytics property, it may take 24-48 hours for your data to become visible and for the reports to be generated.

google analytics home page image

During this waiting period, you can ensure that your tracking code is functioning correctly by checking real-time reports, using the debug view, or examining your website's source code. Additionally, please note that there may be a delay in the real-time reports appearing on the screen.

Tip :

In case you prefer a lightweight analytics solution without the hassle of Google Analytics, especially if you're focused on simple metrics like page views and route-specific statistics, consider giving Vercel Analytics a try. Designed for those using the Vercel platform, it's not only easy to set up but also a powerful tool for gaining quick and insightful data.

Vercel Web Analytics

Vercel Web Analytics

This article was originally published on my website idboussadel.me.

Top comments (2)

Collapse
 
spencermarx profile image
Spencer Marx • Edited

Nice article Abdessamad!

In case you or anyone else here wants an example of how to implement GTM and Consent Mode in NextJS 15 (App Router) feel free to check out the following article 🚀

Happy developing 🧑‍💻⚡

Collapse
 
peterlidee profile image
Peter Jacxsens

Nice work but I was testing this out and it seems that GA4 is smart enough to detect route changes by itself (when improved measurements > pageviews > pageviews based on browserhistory changes is active (by default)). In that case the entire gtagHelper.ts functionality is not necessary. It may in fact cause double page views?? (not sure about this)