Google Publisher Tag for DFP in a Vue.js Single-Page Application
I’ve written A better approach to include GPT Ads in a Vue.js Single-Page Application .
I’ve recently contributed to re-coding a legacy Symfony 1.x/jQuery website as a Vue.js SPA consuming a Python Lambda-based API. Improvement in terms of page load speed, total page size and number of requests has been great. With the same features and identical UI, we achieved an average decrease of 55% in page load time and requests and almost a 70% in downloaded data (images were poorly optimized, no gzip compression enabled, lots of dead and unreachable code…)
Anyway, I’d like to write about the solution I’ve used to have the Ads properly running with Google Publisher Tag within a Single-Page Application. The site is some kind of search engine so, to simplify, let’s consider it has only a couple of templates:
- Home page, with two Ad Units.
- Results page, with four Ad Units.
Only one of these units is common in both templates: a 728×90 Leaderboard above the Header. The main layout would be something as simple as:
<div id="app">
<Advertising :id="'MySite_Home_Leaderboard'" class="leaderboard"/>
<Header/>
<router-view/>
<Footer/>
</div>
For demo purposes, let’s consider the <Advertising/>
component just renders an empty <div>
with the given ID. The
<router-view/>
element loads the main content either if it’s the Home or the results.
How do I display Ads in my Single-Page Application?#
- Include GPT scripts.
- Define the required Ad Units for the current URL.
- Insert the empty
div
elements for the Ads in the DOM. - Call the Display function for every slot in the page.
Obviously, we want our Ads to be displayed as soon as possible, so the goal here is to make the required calls the sooner the better.
1. Including Google Publisher Tag Scripts#
GPT must be included in all URLs so, the best place for this script is the entry point to the SPA, the index.html
template file. Ideally at the top of the <head>
tag. That’d be:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Google Publisher Tags -->
<script type="text/javascript">
var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];
// ...
Just keep in mind:
The
<meta>
element declaring the encoding must be inside the<head>
element and within the first 1024 bytes of the HTML as some browsers only look at those bytes before choosing an encoding. [MDN]
2. Defining the required Ad Units#
If all our pages were to have the same Ad Units, index.html
would be the best place to define them. But that’s not the
case and, of course, Vue.js doesn’t allow using script
tags inside templates either:
Templates should only be responsible for mapping the state to the UI. Avoid placing tags with side-effects in your templates, such as
script
, as they will not be parsed.
The obvious alternative was using Vue’s Lifecycle hooks to define the Ads. But, which ones? in which component?
Besides the leaderboard we’ve seen on the App Component, all the other Ads are on the component associated to the route
that’ll be used in the <router-view/>
(or its children), so considering how the lifecycle for parent/child
components
takes place, the safest
place to call googletag.defineSlot()
seems to be the mounted
hook in the current route component, since the call
requires all Advertising
tags to be rendered in the DOM.
To define the slots I’ve created a src/common/ads.js
module with:
function defineSlots(slots) {
slots.forEach(slot => {
window.ads[slot.opt_div] = googletag.defineSlot(
slot.adUnitPath,
slot.size,
slot.opt_div
);
});
}
I use a global window.ads
to store references to the returned googletag.Slot
objects. This module also exports some
functions that define the required slots for different templates:
export function slotsResultsPage() {
const slots = [
{
adUnitPath: "/1234567/sports",
size: [728, 90],
opt_div: "div-1"
},
// ...
];
defineSlots(slots);
}
I use this slotsResultsPage
function as a created
hook in ResultsPageComponent
. Obviously, the idea is to reuse
this function (probably with a more appropiate name) for every template with the same Ad slots.
import { slotsResultsPage as created } from "@/common/ads";
export default {
name: "ResultsPageComponent",
components: {
// ...
},
created,
// ...
}
3. Inserting the empty div
elements to contain the Ads#
Before calling the googletag.display
method we have to be sure that the DOM nodes for them are already there.
The display call must not happen until the element is present in the DOM. [GPT Reference]
A simplified version of my Advertising
component would be something like:
Vue.component("Advertising", {
props: {
id: String,
},
template: '<div :id="id" class="mysite-ad"></div>'
});
This is actually so simple that wouldn’t require a component at all, but this way you can include other features (a close button, collect stats…) or e.g. to add some new class to all Ads across the site, you just have to update your Ad component’s template.
Insert ads wherever you want as we’ve seen in the intro:
<Advertising :id="MySite_Home_Leaderboard" />
4. Displaying Ads#
Probably, the safest place to do it all at once is on the page’s mounted
hook. To achieve that, I add a displayAds
method to my common ads module:
export function displayAds() {
document.querySelector(".mysite-ad").forEach(node => {
googletag.cmd.push(() => googletag.display(node.id));
});
}
And I use it on the page component (let’s complete the former code):
import { slotsResultsPage as created } from "@/common/ads";
import { displayAds as mounted } from "@/common/ads";
export default {
name: "ResultsPageComponent",
components: {
// ...
},
created,
mounted,
// ...
}
That should be enough to show Ads in our site. The thing is, since this is a Single-Page Application, once we navigate we will see some errors in Google Publisher Tag’s Console . I’m still messing around to find the best way to handle this situation, but one thing that works perfectly fine is to destroy all the slots when loading a new URL.
The easiest way to do it would be to add a destroySlots
function in our commons ads module:
export function destroySlots() {
if (Object.keys(window.ads).length) {
googletag.destroySlots(Object.values(window.ads));
}
// Or just googletag.destroySlots();
}
Of course, we need to destroy the slots before the execution of the created
hook (that will create the new ones again)
for the Page Component we’re navigating to. We can achieve that in many Router Navigation
Guards
or even in Page
Component’s beforeCreate
lifecycle hook:
import { destroySlots as beforeCreate } from "@/common/ads";
import { slotsResultsPage as created } from "@/common/ads";
import { displayAds as mounted } from "@/common/ads";
export default {
name: "ResultsPageComponent",
components: {
// ...
},
beforeCreate,
created,
mounted,
// ...
}
Disclaimer: I’m not a GPT/DFP expert at all but I’ve spent a decent amount of time trying to figure out how to deal with Ads in a Vue.js SPA. The setup I tried to explain in this article worked well for me but it might probably be improved (e.g. I suspect there’s no actual reason to destroy all slots when changing routes, some of them maybe you could just reuse). Anyway, I’ll be glad if this can help anyone. Comments, questions, doubts, other approaches/theories, experiences are more than welcome.