DEV Community

Monterail
Monterail

Posted on

Google Publisher Tag Ads in Single Page Application (Next.js) [CASE STUDY]

In one of the projects I worked in, ads were the only source of income for the client, so they had to be implemented properly. Ad agency hired by client used Google Publisher Tag (GPT) ads, which is a popular choice.

At first, it seemed like a quite simple task — the agency provided all pieces of code that just had to be pasted in the website. It turned out that these codes were good for a simple static website, but to make it work properly in SPA (we’re talking Next.js app here) it required some customization.

The topic is not documented that well, so I had to do a lot of experiments and email exchanges with the ad agency to make it work smoothly. In this article, I'll present my approach to this problem.

How the ads work?

In our case, the agency provided a text file with code for every ad on the website with recommendations on where to place them. Below, I'll show an example of that code block with my comments and try to explain what's happening there (if you want to learn more about GPT, here is official documentation.

<!--
  First two scripts are simple:
  load GPT library and initialize googletag and the command queue.
  They can be placed in the site's <head>
-->
<script
  async="async"
  src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"
></script>
<script>
  var googletag = googletag || {};
  googletag.cmd = googletag.cmd || [];
</script>

<!--
  This script creates "ad unit" that will be then displayed on the page
-->
<script>
  googletag.cmd.push(function () {
    /**
      Mapping assigns sizes of an ad unit to corresponding breakpoints,
      it makes the ad responsive
    */
    var mapping = googletag
      .sizeMapping()
      .addSize([1100, 0], [[750, 200]])
      .addSize([960, 0], [[468, 60]])
      .addSize([0, 0], [])
      .build();

    /**
      Now the slot is being defined.
      The function accepts three arguments:
        - id of the slot (set by the agency in their ad management console),
        - array of sizes (also set by them),
        - id of the div in which the ad should be displayed
    */
    googletag
      .defineSlot(
        "/52555387/XYZ.pl_750x200_7_a_d",
        [
          [750, 200],
          [468, 60],
        ],
        "div-gpt-ad-XYZ.pl_750x200_7_a_d"
      )
      .defineSizeMapping(mapping)
      .addService(googletag.pubads());
    googletag.enableServices();
  });
</script>

<!--
  The part below should be placed in the body of the page
  It is a div with id the same as in "defineSlot()" function
  and a script that displays a defined slot inside that div
-->
<div id="div-gpt-ad-XYZ.pl_750x200_7_a_d">
  <script>
    googletag.cmd.push(function () {
      googletag.display("div-gpt-ad-XYZ.pl_750x200_7_a_d");
    });
  </script>
</div>
Enter fullscreen mode Exit fullscreen mode

So to wrap it up — what happens when you create a page with a code like above?

  1. GPT script is loaded.
  2. Ad slot with a specific ID is defined with its size and a target div's ID. It then stays in the memory.
  3. googletag.display() function is executed that makes a request for the specific ad slot.
  4. HTML fetched in the request above is being placed inside the div. It is usually an <iframe> that displays the ad.

The most important things here:

  • Defined ad slot stays in the memory until page reload.
  • An ad with a specific ID can only be displayed once on the page — trying to run googletag.display() on an already displayed ID will not work.

The problem

So the initial approach was simple — put the code pieces in appropriate places. And it kinda worked — when the page was opened, it displayed all the ads. Perfect! But when we tried to navigate on the page, the ads started to disappear. Why?

The app is written in Next.js (Server-Side Rendering), so on the initial load we get an already rendered HTML with all the ad codes and everything works the same as if it was a simple static page. But when we navigate on the page it isn't reloaded — there is only hydration. So there are two problems when navigating between pages:

  1. googletag.display() is not executed so new ads are not fetched.
  2. Used ad slots stay in the memory, so the GPT script treats them as already displayed.

Solution

To make it work, we have to recreate the ad codes like we do this in React. This also gives us an opportunity for some code reuse.

The desired behavior is:

  • when the ad component is rendered, it defines the ad slot and displays it,
  • when route is changed, all defined slots are removed from the memory,
  • after going to a different page, ad components on that page define new slots and display them.

As you can see in the example ad code, the only things that differentiate the slots are: ID, sizes, and mapping. So it's natural to put this data in a separate file:

const.js


const ads = {

  "750x200_7_a_d": {
    sizes: [[750, 200],[468, 60]],
    mapping: {
      0: [],
      960: [468, 60],
      1100: [750, 200],
    }
  },

  ...
}
Enter fullscreen mode Exit fullscreen mode

The whole functionality of defining the ad slot and displaying it can be put inside a hook so that the code can be reused if we would like to have different components that display the ad:

useAdSlot.js


import { useEffect } from "react";

export function useAdSlot({ mapping, sizes, id, isTransitioning }) {
  useEffect(() => {
    if (!isTransitioning && typeof window !== undefined) {
      const { googletag } = window;
      googletag.cmd.push(function () {
        const adMapping = googletag.sizeMapping();
        Object.keys(mapping).forEach((breakpoint) => {
          adMapping.addSize([Number(breakpoint), 0], [mapping[breakpoint]]);
        });
        const builtMapping = adMapping.build();

        googletag
          .defineSlot(
            `/52555387/XYZ.pl_${id}`,
            sizes,
            `div-gpt-ad-XYZ.pl_${id}`
          )
          .defineSizeMapping(builtMapping)
          .addService(googletag.pubads());
        googletag.enableServices();
      });

      googletag.cmd.push(function () {
        googletag.display(`div-gpt-ad-XYZ.pl_${id}`);
      });
    }
  }, [mapping, sizes, id, isTransitioning]);
}
Enter fullscreen mode Exit fullscreen mode

The hook can be used in Ad component that takes the adId as a prop and displays an empty div that will be then filled with actual ad:

Ad.js


import React from "react";

import { useTransitionState } from "@/containers/TransitionState";
import { useAdSlot } from "@/hooks/useAds";
import { ads } from "./const";

function Ad({ adId }) {
  const { isTransitioning } = useTransitionState();
  const ad = ads[adId];

  useAdSlot({
    mapping: ad.mapping,
    sizes: ad.sizes,
    id: adId,
    isTransitioning,
  });

  return <div id={`div-gpt-ad-XYZ.pl_${adId}`} />;
}

export default Ad;
Enter fullscreen mode Exit fullscreen mode

Note the isTransitioning argument passed to the hook above. Basically, we want to be sure that we run the GPT functions every time we change the page. There are different ways of doing that, but in the project it worked best with the transition state that was already in the app (the state was for displaying a loader when the user is navigating between pages — it was implemented with next.js router events).

Ok, so the first problem is solved — the GPT functions that display the ads run on every page change. But there is still an issue with old ad slots that stay in the memory. Here is where the destroySlots() function comes to the rescue. When called without arguments it removes all data related to displayed ads from the internal GPT state. When to run it? Ideally — after we redirect to another page but before other Ad components start displaying more ad units. And here next.js routing and its events come in handy again. We already manage the transition state in the main next.js App component, so we can just add destroySlots() function there.

Below, I'm showing a piece of code from our main App component. You can see there the methods that manage the transition state (with destroySlots() added) and how it is connected to next.js router events. The value of isTransitioning is then passed to Ad components via context (note that our App was a class component, but it could be easily recreated if yours is functional).

 setTransitionStarted = () => {
    this.setState({ isTransitioning: true });

    // destroy all ad slots
    const { googletag } = window;
    googletag.cmd.push(function () {
      googletag.destroySlots();
    });
  };

  setTransitionComplete = () => {
    this.setState({ isTransitioning: false });
  };

  componentDidMount() {
    Router.events.on("routeChangeStart", this.setTransitionStarted);
    Router.events.on("routeChangeComplete", this.setTransitionComplete);
  }

  componentWillUnmount() {
    Router.events.off("routeChangeStart", this.setTransitionStarted);
    Router.events.off("routeChangeComplete", this.setTransitionComplete);
  }
Enter fullscreen mode Exit fullscreen mode

Now the whole ad lifecycle works as desired. The ad slots are defined and displayed after every page transition and destroyed before going to another page.

Conclusion

Although the problem isn't very complex, it required some experimenting and a better understanding of GPT to solve it. At the end, everything works well and we also got additional benefit from rewriting the ads to React components and hooks: the ad usage in the app became very easy. When adding a new ad unit, we just need to add a new ID with sizes and mapping to config file, and then place the ad component with that ID in the desired place. Similarly, when we want to change the e.g. breakpoints for a specific ad, then it's just a matter of editing the config file.

Originally published on Monterail blog

Top comments (2)

Collapse
 
falcong2001 profile image
Guru Krish S

There is no router events in nextjs 13. Do you have any other idea of solving this problem?

Collapse
 
tranquyetdev profile image
Quyet Tran

Great article. We are facing an issue with Google Publisher Console. The destroyed slots remain in the console. Is there any way to remove them, only show the slots of the current page?
Thanks