├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── apis
│ └── dfp.ts
├── components
│ ├── Ad
│ │ └── index.tsx
│ └── AdsProvider
│ │ └── index.tsx
├── contexts
│ └── ads.ts
├── index.ts
└── types
│ └── index.ts
└── tsconfig.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-typescript", "@babel/preset-react"],
3 | "plugins": [
4 | "@babel/plugin-proposal-object-rest-spread",
5 | "@babel/plugin-proposal-optional-chaining",
6 | "@babel/plugin-proposal-class-properties"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | node_modules
3 | pkg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 BlackBox Vision
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next Google DFP [](https://badge.fury.io/js/%40blackbox-vision%2Fnext-google-dfp) [](https://opensource.org/licenses/MIT)
2 |
3 | 🚀 Integrate Google Double Click for Publishers in Next.js with ease
4 |
5 | ## Installation
6 |
7 | ### NPM
8 |
9 | ```bash
10 | npm i @blackbox-vision/next-google-dfp
11 | ```
12 |
13 | ### YARN
14 |
15 | ```bash
16 | yarn add @blackbox-vision/next-google-dfp
17 | ```
18 |
19 | ## Integration Steps
20 |
21 | ### Define your slots
22 |
23 | ```js
24 | export const ads = [
25 | {
26 | slotId: "/6355419/Travel/Europe/France/Paris",
27 | sizeMappings: [300, 250],
28 | divId: "banner-ad",
29 | },
30 | ];
31 | ```
32 |
33 | ### Add a Provider in Custom App
34 |
35 | ```ts
36 | import { AdsProvider } from "@blackbox-vision/next-google-dfp";
37 |
38 | import { ads } from "../constants/ads";
39 |
40 | function MyApp({ Component, pageProps }) {
41 | return (
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | export default MyApp;
49 | ```
50 |
51 | ### Add an Ad in your page
52 |
53 | ```js
54 | import { Ad } from "@blackbox-vision/next-google-dfp";
55 |
56 | function Page() {
57 | return ;
58 | }
59 |
60 | export default Page;
61 | ```
62 |
63 | ## Responsive Ad Slots
64 |
65 | Responsive slots can be defined with this structure:
66 |
67 | ```js
68 | export const ads = [
69 | {
70 | slotId: "/6355419/Travel/Europe/France/Paris",
71 | sizeMappings: [
72 | {
73 | breakpoint: [1024, 768],
74 | sizes: [
75 | [980, 90],
76 | [980, 250],
77 | [728, 90],
78 | ],
79 | }, //viewport >1024px
80 | {
81 | breakpoint: [0, 0],
82 | sizes: [
83 | [320, 100],
84 | [320, 50],
85 | ],
86 | }, //viewport <1024px
87 | ],
88 | divId: "banner-ad",
89 | },
90 | ];
91 | ```
92 |
93 | // TODO: add props, add support for more cases, show targeting support
94 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@blackbox-vision/next-google-dfp",
3 | "version": "0.4.1",
4 | "description": "Integrate Google DFP in next.js with ease",
5 | "author": "JonatanSalas ",
6 | "homepage": "https://github.com/BlackBoxVision/next-google-dfp#readme",
7 | "license": "MIT",
8 | "scripts": {
9 | "clean": "rimraf pkg",
10 | "publish": "pika publish",
11 | "build": "npm run clean && pika build",
12 | "version": "npm run build"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/BlackBoxVision/next-google-dfp/issues"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.11.4",
19 | "@babel/plugin-proposal-class-properties": "^7.10.4",
20 | "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
21 | "@babel/plugin-proposal-optional-chaining": "^7.14.2",
22 | "@babel/preset-react": "^7.10.4",
23 | "@babel/preset-typescript": "^7.10.4",
24 | "@pika/pack": "^0.5.0",
25 | "@pika/plugin-build-types": "^0.9.2",
26 | "@pika/plugin-build-web": "^0.9.2",
27 | "@pika/plugin-standard-pkg": "^0.9.2",
28 | "@types/react": "^16.9.48",
29 | "@types/react-dom": "^17.0.8",
30 | "next": "^11.0.1",
31 | "react": "^16.13.1",
32 | "rimraf": "^3.0.2",
33 | "typescript": "^3.9.7"
34 | },
35 | "peerDependencies": {
36 | "react": ">=16.11.0",
37 | "next": ">=11.0.0"
38 | },
39 | "@pika/pack": {
40 | "pipeline": [
41 | [
42 | "@pika/plugin-standard-pkg"
43 | ],
44 | [
45 | "@pika/plugin-build-web"
46 | ],
47 | [
48 | "@pika/plugin-build-types"
49 | ]
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/apis/dfp.ts:
--------------------------------------------------------------------------------
1 | import { AdItem } from "../types";
2 |
3 | const REFRESH_KEY = "refresh";
4 | const REFRESH_VALUE = "true";
5 |
6 | // Number of seconds to wait after the slot becomes viewable.
7 | const SECONDS_TO_WAIT_AFTER_VIEWABILITY = 60;
8 |
9 | const googleTag = () => {
10 | const global = window as any;
11 |
12 | global.googletag = global.googletag || {};
13 | global.googletag.cmd = global.googletag.cmd || [];
14 |
15 | return global.googletag;
16 | };
17 |
18 | export const dfp = {
19 | createSlots: (ads: AdItem[], enableLazyload: boolean, enableRefresh: boolean = true) => {
20 | googleTag().cmd.push(() => {
21 | googleTag().pubads().collapseEmptyDivs();
22 |
23 | ads.forEach(({ slotId, divId, sizeMappings }: AdItem) => {
24 | let responsiveMappings: any = null;
25 | let mappings: any = sizeMappings;
26 |
27 | if (sizeMappings.length > 0) {
28 | const [firstSizeMapping]: any = sizeMappings;
29 |
30 | if (
31 | typeof firstSizeMapping === "object" &&
32 | !!firstSizeMapping.breakpoint &&
33 | !!firstSizeMapping.sizes
34 | ) {
35 | const sizeMapping = googleTag().sizeMapping();
36 |
37 | mappings = [];
38 |
39 | sizeMappings.forEach(({ breakpoint, sizes }: any) => {
40 | sizeMapping.addSize(breakpoint, sizes);
41 |
42 | const [firstSize] = sizes;
43 |
44 | if (!!firstSize && Array.isArray(firstSize)) {
45 | mappings.push(...sizes);
46 | } else {
47 | mappings.push(sizes);
48 | }
49 | });
50 |
51 | responsiveMappings = sizeMapping.build();
52 | }
53 | }
54 |
55 | const slot = googleTag()
56 | .defineSlot(slotId, mappings, divId)
57 | .setTargeting(REFRESH_KEY, REFRESH_VALUE)
58 | .addService(googleTag().pubads());
59 |
60 | if (!!responsiveMappings) {
61 | slot.defineSizeMapping(responsiveMappings);
62 | }
63 | });
64 |
65 | if (enableRefresh) {
66 | googleTag()
67 | .pubads()
68 | .addEventListener("impressionViewable", (event) => {
69 | const slot = event.slot;
70 |
71 | if (slot.getTargeting(REFRESH_KEY).indexOf(REFRESH_VALUE) > -1) {
72 | setTimeout(() => {
73 | googleTag().pubads().refresh([slot]);
74 | }, SECONDS_TO_WAIT_AFTER_VIEWABILITY * 1000);
75 | }
76 | });
77 | }
78 |
79 | if (!!enableLazyload) {
80 | // Enable lazyload with some good defaults
81 | googleTag().pubads().enableLazyLoad({
82 | fetchMarginPercent: 500,
83 | renderMarginPercent: 200,
84 | mobileScaling: 2.0,
85 | });
86 | }
87 |
88 | googleTag().enableServices();
89 | });
90 | },
91 | showSlot: (divId: string) => {
92 | googleTag().cmd.push(() => {
93 | googleTag().display(divId);
94 | });
95 | },
96 | removeSlots: () => {
97 | googleTag().cmd.push(() => {
98 | googleTag().destroySlots();
99 | });
100 | },
101 | };
102 |
--------------------------------------------------------------------------------
/src/components/Ad/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 |
3 | import { AdComponent } from "../../types";
4 |
5 | import { useAdsContext } from "../../contexts/ads";
6 | import { dfp } from "../../apis/dfp";
7 |
8 | export const Ad: AdComponent = ({
9 | id,
10 | className,
11 | style = {},
12 | width = 350,
13 | height = 250,
14 | }) => {
15 | const { isLoading } = useAdsContext();
16 |
17 | useEffect(() => {
18 | if (!isLoading && !!id) {
19 | dfp.showSlot(id);
20 | }
21 | }, [isLoading, id]);
22 |
23 | return (
24 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/AdsProvider/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useRouter } from "next/router";
3 | import Script from "next/script";
4 |
5 | import { AdsProviderComponent } from "../../types";
6 |
7 | import { AdsContext } from "../../contexts/ads";
8 | import { dfp } from "../../apis/dfp";
9 |
10 | export const AdsProvider: AdsProviderComponent = ({
11 | ads,
12 | children,
13 | debug = false,
14 | enableLazyload = true,
15 | enableRefresh = true
16 | }) => {
17 | const [isLoading, setIsLoading] = useState(true);
18 |
19 | const router = useRouter();
20 |
21 | // Create ad slots
22 | useEffect(() => {
23 | setIsLoading(true);
24 |
25 | dfp.createSlots(ads, enableLazyload, enableRefresh);
26 |
27 | setIsLoading(false);
28 |
29 | const handleRouteChangeStart = (url) => {
30 | if (window.location.pathname !== url) {
31 | setIsLoading(true);
32 | dfp.removeSlots();
33 | dfp.createSlots(ads, enableLazyload, enableRefresh);
34 | }
35 | };
36 |
37 | const handleRouteChangeComplete = () => {
38 | setIsLoading(false);
39 | };
40 |
41 | router.events.on("routeChangeStart", handleRouteChangeStart);
42 | router.events.on("routeChangeComplete", handleRouteChangeComplete);
43 |
44 | return () => {
45 | router.events.off("routeChangeStart", handleRouteChangeStart);
46 | router.events.off("routeChangeComplete", handleRouteChangeComplete);
47 | };
48 | }, [ads, enableLazyload, enableRefresh]);
49 |
50 | // Enable debug console if possible
51 | useEffect(() => {
52 | const searchParams = new URLSearchParams(window.location.search);
53 |
54 | if (!!debug && !searchParams.has("google_console")) {
55 | searchParams.append("google_console", "1");
56 | window.location = `${window.location.pathname}?${searchParams}` as any;
57 | }
58 |
59 | if (!debug && searchParams.has("google_console")) {
60 | searchParams.delete("google_console");
61 |
62 | const search = `${searchParams}`.length > 0 ? `?${searchParams}` : "";
63 | window.location = `${window.location.pathname}${search}` as any;
64 | }
65 | }, [debug]);
66 |
67 | return (
68 | <>
69 |
70 | {children}
71 |
72 |
76 | >
77 | );
78 | };
79 |
--------------------------------------------------------------------------------
/src/contexts/ads.ts:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 |
3 | import { AdsContextValue } from "../types";
4 |
5 | export const AdsContext = React.createContext(null);
6 |
7 | export const useAdsContext = (): AdsContextValue => {
8 | const context = useContext(AdsContext);
9 |
10 | if (!context) {
11 | throw new Error(
12 | `[AdsContext]: You forgot to wrap your component with AdsProvider`
13 | );
14 | }
15 |
16 | return context;
17 | };
18 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./types";
2 |
3 | export * from "./components/Ad";
4 | export * from "./components/AdsProvider";
5 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export type FluidItem = ["fluid"];
4 | export type SizeItem = [number, number];
5 | export type ResponsiveItem = {
6 | breakpoint: SizeItem;
7 | sizes: SizeItem | SizeItem[];
8 | };
9 |
10 | export type SizeMappings = FluidItem | SizeItem | SizeItem[] | ResponsiveItem[];
11 |
12 | export type AdItem = {
13 | divId: string;
14 | slotId: string;
15 | sizeMappings: SizeMappings;
16 | };
17 |
18 | // Context
19 |
20 | export type AdsContextValue = null | {
21 | isLoading: boolean;
22 | };
23 |
24 | // Components
25 |
26 | type AdsProviderProps = React.PropsWithChildren<{
27 | ads: AdItem[];
28 | debug?: boolean;
29 | enableLazyload?: boolean;
30 | enableRefresh?: boolean;
31 | }>;
32 |
33 | export type AdsProviderComponent = React.FC;
34 |
35 | type AdProps = {
36 | id: string;
37 | style?: React.CSSProperties
38 | className?: string;
39 | width: number | string;
40 | height: number | string;
41 | };
42 |
43 | export type AdComponent = React.FC;
44 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "/dist",
4 | "rootDirs": ["src"],
5 | "baseUrl": "src",
6 | "module": "commonjs",
7 | "target": "es5",
8 | "lib": ["es5", "es6", "es7", "es2017", "DOM"],
9 | "sourceMap": true,
10 | "allowJs": false,
11 | "jsx": "react",
12 | "moduleResolution": "node",
13 | "forceConsistentCasingInFileNames": true,
14 | "noImplicitReturns": false,
15 | "noImplicitThis": false,
16 | "noImplicitAny": false,
17 | "strictNullChecks": false,
18 | "suppressImplicitAnyIndexErrors": true,
19 | "noUnusedLocals": false,
20 | "declaration": true,
21 | "allowSyntheticDefaultImports": true,
22 | "experimentalDecorators": true,
23 | "emitDecoratorMetadata": true,
24 | "esModuleInterop": true
25 | },
26 | "include": ["src/**/*"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------