Google Publisher Tag (GPT) 官方 Next.js 版本示例
在基于 Next.js 的基本单页应用 (SPA) 中部署 Google 发布商代码 (GPT)。
来源:https://developers.google.com/publisher-tag/samples/integrations/react?hl=zh-cn
官方示例下载:
要点:
- 确保 GPT 仅加载一次。
- 使用 disableInitialLoad() 和 refresh() 控制何时发出广告请求。参阅控制广告加载和刷新。
- 使用 setTargeting() 和 clearTargeting() 可控制应用于广告位的定位条件。参阅键值对定位。
- 使用 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;
}