Google Publisher Tag (GPT) 官方 Next.js 版本示例

在基于 Next.js 的基本单页应用 (SPA) 中部署 Google 发布商代码 (GPT)。

来源:https://developers.google.com/publisher-tag/samples/integrations/react?hl=zh-cn

官方示例下载:

要点:

  1. 确保 GPT 仅加载一次。
  2. 使用 disableInitialLoad() 和 refresh() 控制何时发出广告请求。参阅控制广告加载和刷新
  3. 使用 setTargeting() 和 clearTargeting() 可控制应用于广告位的定位条件。参阅键值对定位
  4. 使用 destroySlots() 清理不再需要的广告位(例如,由于组件被卸载而移除广告位容器时)。

在 StackBlitz 上查看官方示例:

主要结构:

- components/
  - google-publisher-tag.js
  - layout.js
  - layout.module.css

- lib/
  - samples.js

- pages/
  - samples/
    - [id].js
  - index.js

- samples/
  - fixed-size-ad.json
  - fluid-ad.json
  - multiple-fixed-size-ads.json
  - multisize-ad.json

- styles/
  - shared.module.css

// components/google-publisher-tag.js,主要组件

/**
 * @license
 * Copyright 2023 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { useEffect } from 'react';

import sharedStyles from '../styles/shared.module.css';

// Official GPT sources.
const GPT_STANDARD_URL = 'https://securepubads.g.doubleclick.net/tag/js/gpt.js';
const GPT_LIMITED_ADS_URL =
  'https://pagead2.googlesyndication.com/tag/js/gpt.js';

// Keep track of defined ad slots.
let adSlots = {};
let adSlotCount = 0;

if (typeof window !== 'undefined') {
  // Ensure we can interact with the GPT command array.
  window.googletag = window.googletag || { cmd: [] };

  // Prepare GPT to display ads.
  googletag.cmd.push(() => {
    // Disable initial load, to precisely control when ads are requested.
    googletag.pubads().disableInitialLoad();

    // Enable SRA and services.
    googletag.pubads().enableSingleRequest();
    googletag.enableServices();
  });
}

export function InitializeGPT({ limitedAds }) {
  // Reset tracking variables.
  adSlots = {};
  adSlotCount = 0;

  return (
    <script src={limitedAds ? GPT_LIMITED_ADS_URL : GPT_STANDARD_URL} async />
  );
}

export function DefineAdSlot({ adUnit, size }) {
  const slotId = `slot-${adSlotCount++}`;

  useEffect(() => {
    // Register the slot with GPT when the component is loaded.
    googletag.cmd.push(() => {
      const slot = googletag.defineSlot(adUnit, size, slotId);
      if (slot) {
        slot.addService(googletag.pubads());
        googletag.display(slot);
        adSlots[slotId] = slot;
      }
    });

    // Clean up the slot when the component is unloaded.
    return () => {
      googletag.cmd.push(() => {
        if (adSlots[slotId]) {
          googletag.destroySlots([adSlots[slotId]]);
          delete adSlots[slotId];
        }
      });
    };
  }, []);

  // Create the ad slot container.
  return (
    <div
      className={`${sharedStyles.adSlot} ${sharedStyles.centered}`}
      style={getMinimumSlotSize(size)}
      id={slotId}
    ></div>
  );
}

export function RequestAds() {
  useEffect(() => {
    googletag.cmd.push(() => {
      // Request ads for all ad slots defined up to this point.
      //
      // In many real world scenarios, requesting ads for *all*
      // slots is not optimal. Instead, care should be taken to
      // only refresh newly added/updated slots.
      const slots = Object.values(adSlots);
      googletag.pubads().refresh(slots);
    });
  }, []);
}

/**
 * Determine minimum width and height values for an ad slot container
 * based on the configured slot sizes.
 *
 * This function is only provided for example purposes. See
 * [Minimize layout shift](https://developers.google.com/publisher-tag/guides/minimize-layout-shift)
 * to learn more about strategies for sizing ad slot containers.
 */
function getMinimumSlotSize(size) {
  const maxValue = Number.MAX_VALUE;

  let minW = Number.MAX_VALUE;
  let minH = Number.MAX_VALUE;

  if (Array.isArray(size)) {
    // Convert googletag.SingleSize to googletag.MultiSize for convenience.
    const sizes = size.length <= 2 && !Array.isArray(size[0]) ? [size] : size;

    for (const size of sizes) {
      if (Array.isArray(size) && size[0] !== 'fluid') {
        minW = Math.min(size[0], minW);
        minH = Math.min(size[1], minH);
      }
    }
  }

  return minW < maxValue && minH < maxValue
    ? // Static ad slot.
      { minWidth: `${minW}px`, minHeight: `${minH}px` }
    : // Fluid ad slot.
      { minWidth: '50%' };
}

// components/layout.js,页面布局

import Head from 'next/head';
import Link from 'next/link';

import { InitializeGPT } from './google-publisher-tag';
import styles from './layout.module.css';

export default function Layout({ children, home }) {
  return (
    <div className={styles.content}>
      <Head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <InitializeGPT />
      </Head>
      <main>{children}</main>
      {!home && (
        <div className={styles.home}>
          <Link href="/">← Back to home</Link>
        </div>
      )}
    </div>
  );
}

// components/layout.module.css,布局样式

.content {
  padding: 0 1rem;
  margin: 3rem auto 3rem;
}

.home {
  margin: 2rem 0 0;
}

// lib/samples.js,样例处理

/**
 * @license
 * Copyright 2023 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import fs from 'fs';
import path from 'path';

const SAMPLE_DIR = path.join(process.cwd(), 'samples');

export function getSortedSamples() {
  // Get file names under /samples.
  const fileNames = fs.readdirSync(SAMPLE_DIR);

  const sampleData = fileNames.map((fileName) => {
    // Use file name without extension as the ID.
    const id = fileName.replace(/\.json$/, '');
    return getSample(id);
  });

  // Sort samples by title.
  return sampleData.sort((a, b) => {
    return a.title > b.title ? 1 : a.title < b.title ? -1 : 0;
  });
}

export function getSampleIds() {
  const fileNames = fs.readdirSync(SAMPLE_DIR);
  return fileNames.map((fileName) => {
    return {
      params: {
        id: fileName.replace(/\.json$/, ''),
      },
    };
  });
}

export function getSample(id) {
  const fullPath = path.join(SAMPLE_DIR, `${id}.json`);
  const fileContents = JSON.parse(fs.readFileSync(fullPath, 'utf8'));

  return {
    id,
    ...fileContents,
  };
}

// pages/samples/[id].js,样例展示模板

/**
 * @license
 * Copyright 2023 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import Head from 'next/head';

import {
  DefineAdSlot,
  RequestAds,
} from '../../components/google-publisher-tag';
import Layout from '../../components/layout';
import { getSample, getSampleIds } from '../../lib/samples';
import sharedStyles from '../../styles/shared.module.css';

export default function Sample({ sample }) {
  return (
    <Layout>
      <Head>
        <title>{sample.title}</title>
      </Head>
      <section className={sharedStyles.centered}>
        {sample.slots.map((slot, i) => (
          <DefineAdSlot adUnit={slot.adUnit} size={slot.size} key={i} />
        ))}
      </section>
      <section>
        <aside className={`${sharedStyles.callout} ${sharedStyles.centered}`}>
          <p className={sharedStyles.condensed}>
            Some samples may not work when loaded in an iframe.
          </p>
          <p className={sharedStyles.condensed}>
            If a sample fails to load, try opening the preview in a new tab.
          </p>
        </aside>
      </section>
      <RequestAds />
    </Layout>
  );
}

export async function getStaticPaths() {
  const paths = getSampleIds();
  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  const sample = getSample(params.id);
  return {
    props: {
      sample,
    },
  };
}

// pages/index.js,首页

/**
 * @license
 * Copyright 2023 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import Head from 'next/head';
import Link from 'next/link';

import Layout from '../components/layout';
import { getSortedSamples } from '../lib/samples';
import sharedStyles from '../styles/shared.module.css';

export default function Home({ samples }) {
  return (
    <Layout home>
      <Head>
        <title>GPT and React sample</title>
      </Head>
      <header className={sharedStyles.centered}>
        <h1>GPT and React</h1>
      </header>
      <section className={sharedStyles.centered}>
        <p>
          This site serves as a basic example of using Google Publisher Tag with{' '}
          <a href="https://react.dev/">React</a> and{' '}
          <a href="https://nextjs.org/">Next.js</a>.
        </p>
        <p>
          The links below lead to pages that load various types of sample ads.
        </p>
      </section>
      <section>
        <h2>Samples</h2>
        <ul>
          {samples.map(({ id, title, description }) => (
            <li key={id}>
              <Link href={`/samples/${id}`}>{title}</Link> - {description}
              <br />
            </li>
          ))}
        </ul>
      </section>
    </Layout>
  );
}

export async function getStaticProps() {
  const samples = getSortedSamples();
  return {
    props: {
      samples,
    },
  };
}

// samples/fixed-size-ad.json,固定尺寸广告样例参数

{
  "title": "Fixed-size ad",
  "description": "Displays a fixed-sized test ad",
  "slots": [
    {
      "adUnit": "/6355419/Travel/Europe/France/Paris",
      "size": [300, 250]
    }
  ]
}

// samples/fluid-ad.json,流式广告样例参数

{
  "title": "Fluid ad",
  "description": "Displays a fluid test ad",
  "slots": [
    {
      "adUnit": "/6355419/Travel",
      "size": ["fluid"]
    }
  ]
}

// samples/multiple-fixed-size-ads.json,多个固定尺寸样例参数

{
  "title": "Multiple fixed-size ads",
  "description": "Display multiple fixed-sized test ads",
  "slots": [
    {
      "adUnit": "/6355419/Travel/Europe",
      "size": [300, 250]
    },
    {
      "adUnit": "/6355419/Travel/Europe",
      "size": [728, 90]
    },
    {
      "adUnit": "/6355419/Travel/Europe",
      "size": [750, 200]
    }
  ]
}

// samples/multisize-ad.json,多尺寸样例参数

{
  "title": "Multi-size ad",
  "description": "Display a multi-sized test ad",
  "slots": [
    {
      "adUnit": "/6355419/Travel/Europe",
      "size": [
        [300, 250],
        [728, 90],
        [750, 200]
      ]
    }
  ]
}

// styles/shared.module.css,广告样式

.centered {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.condensed {
  margin: 0.4rem;
}

.callout {
  background-color: lightyellow;
  border: 1px solid;
  border-radius: 10px;
  margin: 2rem 0 0;
}

.adSlot {
  border: 1px dashed;
  background-color: lightgrey;
  justify-content: center;
  margin: 1rem;
}

.adSlot:empty::before {
  background-color: dimgrey;
  border-radius: 5px;
  color: lightgrey;
  content: 'Ad';
  font: 12px sans-serif;
  padding: 3px;
  text-align: center;
  width: 20px;
}