src/lib/ad-sizes.ts (389 lines of code) (raw):
import { breakpoints, isBreakpoint } from './breakpoint';
import type { Breakpoint } from './breakpoint';
import type { Indices } from './types';
type AdSizeString = 'fluid' | `${number},${number}`;
type BreakpointIndices = Indices<typeof breakpoints>;
/**
* Store ad sizes in a way that is compatible with google-tag but also accessible via
* more semantic `width`/`height` properties and keep things readonly.
*
* example:
* const size = new AdSize([300, 250]);
*
* size.width === 300; // true
* size[0] === 300; // true
*
* size.height === 250; // true
* size[1] === 250; // true
*
* size[0] = 200; // throws error
* size.width = 200; // throws error
*
*/
class AdSize extends Array<number> {
readonly [0]: number;
readonly [1]: number;
constructor([width, height]: [number, number]) {
super();
this[0] = width;
this[1] = height;
}
public toString(): AdSizeString {
return this.width === 0 && this.height === 0
? 'fluid'
: `${this.width},${this.height}`;
}
public toArray(): number[] {
return [this[0], this[1]];
}
// The advert size is not reflective of the actual size of the advert.
// For example, fluid ads and Guardian merch ads are larger than the dimensions
public isProxy(): boolean {
const isOutOfPage = this.width === 1 && this.height === 1;
const isEmpty = this.width === 2 && this.height === 2;
const isFluid = this.toString() === 'fluid';
const isMerch = this.width === 88;
const isSponsorLogo = this.width === 3 && this.height === 3;
return isOutOfPage || isEmpty || isFluid || isMerch || isSponsorLogo;
}
get width(): number {
return this[0];
}
get height(): number {
return this[1];
}
}
type SizeKeys =
| '160x600'
| '300x1050'
| '300x250'
| '300x600'
| '728x90'
| '970x250'
| 'billboard'
| 'cascade'
| 'empty'
| 'fabric'
| 'fluid'
| 'googleCard'
| 'halfPage'
| 'leaderboard'
| 'merchandising'
| 'merchandisingHigh'
| 'merchandisingHighAdFeature'
| 'mobilesticky'
| 'mpu'
| 'outOfPage'
| 'outstreamDesktop'
| 'outstreamGoogleDesktop'
| 'outstreamMobile'
| 'portrait'
| 'portraitInterstitial'
| 'pubmaticInterscroller'
| 'skyscraper'
| 'sponsorLogo';
type SlotName =
| 'article-end'
| 'carrot'
| 'comments-expanded'
| 'comments'
| 'crossword-banner-mobile'
| 'exclusion'
| 'external'
| 'fronts-banner'
| 'inline'
| 'liveblog-top'
| 'merchandising-high'
| 'merchandising'
| 'mobile-sticky'
| 'football-right'
| 'mostpop'
| 'right'
| 'sponsor-logo'
| 'survey'
| 'top-above-nav'
| 'interactive';
type SizeMapping = Partial<Record<Breakpoint, readonly AdSize[]>>;
type SlotSizeMappings = Record<SlotName, SizeMapping>;
const createAdSize = (width: number, height: number): AdSize => {
return new AdSize([width, height]);
};
const namedStandardAdSizes = {
billboard: createAdSize(970, 250),
halfPage: createAdSize(300, 600),
leaderboard: createAdSize(728, 90),
mobilesticky: createAdSize(320, 50),
mpu: createAdSize(300, 250),
portrait: createAdSize(300, 1050),
skyscraper: createAdSize(160, 600),
cascade: createAdSize(940, 230),
portraitInterstitial: createAdSize(320, 480),
};
const standardAdSizes = {
'970x250': namedStandardAdSizes.billboard,
'300x600': namedStandardAdSizes.halfPage,
'728x90': namedStandardAdSizes.leaderboard,
'300x250': namedStandardAdSizes.mpu,
'300x1050': namedStandardAdSizes.portrait,
'160x600': namedStandardAdSizes.skyscraper,
};
const outstreamSizes = {
outstreamDesktop: createAdSize(620, 350),
outstreamGoogleDesktop: createAdSize(550, 310),
outstreamMobile: createAdSize(300, 197),
};
/**
* Ad sizes commonly associated with third parties
*/
const proprietaryAdSizes = {
fluid: createAdSize(0, 0),
googleCard: createAdSize(300, 274),
outOfPage: createAdSize(1, 1),
pubmaticInterscroller: createAdSize(371, 660), // pubmatic only mobile size
};
/**
* Ad sizes associated with in-house formats
*/
const guardianProprietaryAdSizes = {
empty: createAdSize(2, 2),
fabric: createAdSize(88, 71),
merchandising: createAdSize(88, 88),
merchandisingHigh: createAdSize(88, 87),
merchandisingHighAdFeature: createAdSize(88, 89),
/**
* This is a proxy size (not the true size of the rendered creative)
* that can be used to ensure that no other high priority line items
* fill a certain slot.
*/
sponsorLogo: createAdSize(3, 3),
};
const adSizes = {
...namedStandardAdSizes,
...standardAdSizes,
...outstreamSizes,
...proprietaryAdSizes,
...guardianProprietaryAdSizes,
} as const satisfies Record<SizeKeys, AdSize>;
/**
* mark: 432b3a46-90c1-4573-90d3-2400b51af8d0
* Some of these may or may not need to be synced for with the sizes in ./create-ad-slot.ts
* these were originally from DCR, create-ad-slot.ts ones were in frontend.
*
* Note:
* If a breakpoint is not defined in a size mapping for a slot, that breakpoint will use the sizes
* of the next breakpoint down that has a size mapping. For example, if only "mobile" and "phablet" sizes
* are defined for a slot, all breakpoints larger than "phablet" will use the mapping for "phablet".
*
* In another example, if a slot has only "tablet" as a size mapping defined,
* then "desktop" will use the size mapping for "tablet". "mobile" and "phablet"
* will have no size mapping. This type of example may be used in cases where
* we only want the slot to appear on the "tablet" size or greater.
**/
const slotSizeMappings = {
inline: {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.outstreamMobile,
adSizes.mpu,
adSizes.googleCard,
adSizes.fluid,
],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.mpu,
adSizes.googleCard,
adSizes.fluid,
],
},
right: {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.mpu,
adSizes.googleCard,
adSizes.halfPage,
adSizes.fluid,
],
},
comments: {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.outstreamMobile,
adSizes.mpu,
adSizes.googleCard,
adSizes.fluid,
],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.mpu,
adSizes.googleCard,
adSizes.fluid,
],
},
'comments-expanded': {
mobile: [adSizes.mpu, adSizes.empty],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.mpu,
adSizes.googleCard,
adSizes.fluid,
adSizes.skyscraper,
adSizes.halfPage,
],
},
'top-above-nav': {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.fabric,
adSizes.outstreamMobile,
adSizes.mpu,
adSizes.fluid,
],
tablet: [
adSizes.outOfPage,
adSizes.empty,
adSizes.fabric,
adSizes.fluid,
adSizes.leaderboard,
],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.leaderboard,
createAdSize(940, 230),
createAdSize(900, 250),
adSizes.billboard,
adSizes.fabric,
adSizes.fluid,
],
},
'fronts-banner': {
tablet: [adSizes.empty, adSizes.leaderboard],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.billboard,
adSizes.merchandisingHigh,
adSizes.fluid,
],
},
mostpop: {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.mpu,
adSizes.googleCard,
adSizes.fluid,
],
phablet: [
adSizes.outOfPage,
adSizes.empty,
adSizes.outstreamMobile,
adSizes.mpu,
adSizes.googleCard,
adSizes.halfPage,
adSizes.fluid,
],
tablet: [
adSizes.outOfPage,
adSizes.empty,
adSizes.mpu,
adSizes.googleCard,
adSizes.halfPage,
adSizes.fluid,
],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.mpu,
adSizes.googleCard,
adSizes.halfPage,
adSizes.fluid,
],
},
'liveblog-top': {
mobile: [adSizes.outOfPage, adSizes.empty, adSizes.mpu, adSizes.fluid],
tablet: [],
desktop: [],
},
'merchandising-high': {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.merchandisingHigh,
adSizes.fluid,
adSizes.mpu,
],
tablet: [
adSizes.outOfPage,
adSizes.empty,
adSizes.merchandisingHigh,
adSizes.fluid,
],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.merchandisingHigh,
adSizes.fluid,
adSizes.billboard,
],
},
merchandising: {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.merchandising,
adSizes.fluid,
adSizes.mpu,
],
tablet: [
adSizes.outOfPage,
adSizes.empty,
adSizes.merchandising,
adSizes.fluid,
],
desktop: [
adSizes.outOfPage,
adSizes.empty,
adSizes.merchandising,
adSizes.fluid,
adSizes.billboard,
],
},
survey: {
desktop: [adSizes.outOfPage],
},
carrot: {
mobile: [adSizes.fluid],
},
'mobile-sticky': {
mobile: [adSizes.mobilesticky, adSizes.empty, createAdSize(300, 50)],
},
'crossword-banner-mobile': {
mobile: [adSizes.mobilesticky],
},
'football-right': {
desktop: [
adSizes.empty,
adSizes.mpu,
adSizes.skyscraper,
adSizes.halfPage,
],
},
'article-end': {
mobile: [], // Mappings are dynamically added for this slot using additionalSizes
},
exclusion: {
mobile: [adSizes.empty],
},
/**
* @deprecated Use `slotSizeMappings['sponsor-logo']` instead
*/
external: {
mobile: [adSizes.outOfPage, adSizes.empty, adSizes.fluid, adSizes.mpu],
},
'sponsor-logo': {
mobile: [
adSizes.outOfPage,
adSizes.empty,
adSizes.fluid,
adSizes.sponsorLogo,
],
},
interactive: {
// Mappings are dynamically added for this slot using data attributes
mobile: [adSizes.outOfPage, adSizes.empty],
tablet: [adSizes.outOfPage, adSizes.empty],
desktop: [adSizes.outOfPage, adSizes.empty],
},
} as const satisfies SlotSizeMappings;
const getAdSize = (size: SizeKeys): AdSize => adSizes[size];
/**
* Finds the ad sizes that will be used for a breakpoint given a size mapping
*
* If ad sizes are defined in the size mapping for the specified breakpoint, we use that.
* If no sizes are defined for the breakpoint, use the next smallest breakpoint with defined ad sizes.
* If no smaller breakpoints have defined ad sizes, return an empty array
*
* Example:
* For the following slotSizeMappings:
* inline: {
* phablet: [adSizes.mpu],
* desktop: [adSizes.billboard]
* }
* the applied sizes for each breakpoint for the "inline" slot will be:
* mobile: []
* phablet: [adSizes.mpu]
* tablet: [adSizes.mpu]
* desktop: [adSizes.billboard]
*
* See ad-sizes.test.ts for more examples
*/
const findAppliedSizesForBreakpoint = (
sizeMappings: SizeMapping,
breakpoint: Breakpoint,
): readonly AdSize[] => {
if (!isBreakpoint(breakpoint)) {
return [];
}
let breakpointIndex: BreakpointIndices = breakpoints.findIndex(
(b) => b === breakpoint,
) as BreakpointIndices;
while (breakpointIndex >= 0) {
const breakpointToTry: Breakpoint = breakpoints[breakpointIndex];
const sizeMapping = sizeMappings[breakpointToTry];
if (sizeMapping?.length) {
return sizeMapping;
}
breakpointIndex--;
}
// There are no size mappings defined for any size smaller than the breakpoint
return [];
};
// Export for testing
export const _ = { createAdSize };
export type { AdSizeString, SizeKeys, SizeMapping, SlotSizeMappings, SlotName };
export {
AdSize,
adSizes,
standardAdSizes,
outstreamSizes,
getAdSize,
slotSizeMappings,
createAdSize,
findAppliedSizesForBreakpoint,
};