-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathenhance-ad-placeholders.ts
More file actions
218 lines (193 loc) · 6.36 KB
/
enhance-ad-placeholders.ts
File metadata and controls
218 lines (193 loc) · 6.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat';
import type { AdPlaceholderBlockElement, FEElement } from '../types/content';
import type { RenderingTarget } from '../types/renderingTarget';
/**
* Positioning rules:
*
* - Is further than 3 elements in
* - Is at least 6 elements after the previous ad
* - No more than 15 ads in an article
* - Last element should not be followed by an ad
* - Ad should not appear immediately before an image
*/
const isSuitablePosition = (
elementCounter: number,
numberOfAdsInserted: number,
lastAdIndex: number,
prevIsParagraphOrImage: boolean,
isLastElement: boolean,
isParagraph: boolean,
): boolean => {
// Rules for ad placement
const adEveryNElements = 6;
const firstAdIndex = 4;
const maxAds = 15;
// Don't insert more than `maxAds` ads
if (numberOfAdsInserted >= maxAds) {
return false;
}
// Don't insert advert in final position
if (isLastElement) {
return false;
}
// Check that we haven't inserted an ad yet, and that we are far enough in to do so
const isFirstAdIndex = elementCounter >= firstAdIndex && lastAdIndex === 0;
// Check that we are at least `adEveryNElements` elements after the previous ad
const isEnoughElementsAfter =
elementCounter - lastAdIndex >= adEveryNElements;
// Insert an ad placeholder before the current element if it is a paragraph, the
// previous element was an image or a paragraph, and if the position is eligible
return (
isParagraph &&
prevIsParagraphOrImage &&
(isFirstAdIndex || isEnoughElementsAfter)
);
};
const isParagraph = (element: FEElement) =>
element._type === 'model.dotcomrendering.pageElements.TextBlockElement';
// We don't want to insert an ad after an image that isn't full width as it looks bad
const isEligibleImage = (element: FEElement) =>
element._type === 'model.dotcomrendering.pageElements.ImageBlockElement' &&
element.role !== 'thumbnail' &&
element.role !== 'supporting';
const insertPlaceholder = (
prevElements: FEElement[],
currentElement: FEElement,
): FEElement[] => {
const placeholder: AdPlaceholderBlockElement = {
_type: 'model.dotcomrendering.pageElements.AdPlaceholderBlockElement',
};
return [...prevElements, placeholder, currentElement];
};
const insertPlaceholderAfterCurrentElement = (
prevElements: FEElement[],
currentElement: FEElement,
): FEElement[] => {
const placeholder: AdPlaceholderBlockElement = {
_type: 'model.dotcomrendering.pageElements.AdPlaceholderBlockElement',
};
return [...prevElements, currentElement, placeholder];
};
type ReducerAccumulatorGallery = {
elements: FEElement[];
imageBlockElementCounter: number;
};
/**
* Insert ad placeholders for gallery articles.
* Gallery-specific rules:
* - Place ads after every 4th image
* - Start placing ads after the 4th image
* @param elements - The array of elements to enhance
* @returns The enhanced array of elements with ad placeholders inserted
*/
const insertAdPlaceholdersForGallery = (elements: FEElement[]): FEElement[] => {
const elementsWithReducerContext = elements.reduce(
(
prev: ReducerAccumulatorGallery,
currentElement: FEElement,
): ReducerAccumulatorGallery => {
const imageBlockElementCounter =
currentElement._type ===
'model.dotcomrendering.pageElements.ImageBlockElement'
? prev.imageBlockElementCounter + 1
: prev.imageBlockElementCounter;
const shouldInsertAd = imageBlockElementCounter % 4 === 0;
return {
elements: shouldInsertAd
? insertPlaceholderAfterCurrentElement(
prev.elements,
currentElement,
)
: [...prev.elements, currentElement],
imageBlockElementCounter,
};
},
// Initial value for reducer function
{
elements: [],
imageBlockElementCounter: 0,
},
);
return elementsWithReducerContext.elements;
};
/**
* - elementCounter: the number of paragraphs and images that we've counted when considering ad insertion
* - lastAdIndex: the index of the most recently inserted ad, used to calculate the elements between subsequent ads
* - numberOfAdsInserted: we use this to make sure that our total number of ads on an article does not exceed maxAds
* - prevIsAParagraphOrImage: we use this to check whether or not the previous element is suitable to insert an ad
* beneath - we don't want to insert an ad underneath a rich link, for example
*/
type ReducerAccumulator = {
elements: FEElement[];
elementCounter: number;
lastAdIndex: number;
numberOfAdsInserted: number;
prevIsParagraphOrImage: boolean;
};
const insertAdPlaceholders = (elements: FEElement[]): FEElement[] => {
const elementsWithReducerContext = elements.reduce(
(
prev: ReducerAccumulator,
currentElement: FEElement,
idx: number,
): ReducerAccumulator => {
const elementCounter =
isParagraph(currentElement) || isEligibleImage(currentElement)
? prev.elementCounter + 1
: prev.elementCounter;
const shouldInsertAd = isSuitablePosition(
elementCounter,
prev.numberOfAdsInserted,
prev.lastAdIndex,
prev.prevIsParagraphOrImage,
elements.length === idx + 1,
isParagraph(currentElement),
);
const currentElements = [...prev.elements, currentElement];
return {
elements: shouldInsertAd
? insertPlaceholder(prev.elements, currentElement)
: currentElements,
elementCounter,
lastAdIndex: shouldInsertAd ? elementCounter : prev.lastAdIndex,
numberOfAdsInserted: shouldInsertAd
? prev.numberOfAdsInserted + 1
: prev.numberOfAdsInserted,
prevIsParagraphOrImage:
isParagraph(currentElement) ||
isEligibleImage(currentElement),
};
},
// Initial value for reducer function
{
elements: [],
elementCounter: 0,
lastAdIndex: 0,
numberOfAdsInserted: 0,
prevIsParagraphOrImage: false,
},
);
return elementsWithReducerContext.elements;
};
export const enhanceAdPlaceholders =
(
format: ArticleFormat,
renderingTarget: RenderingTarget,
shouldHideAds: boolean,
) =>
(elements: FEElement[]): FEElement[] => {
if (shouldHideAds) return elements;
// In galleries the AdPlaceholders are inserted in both
// Web & App because the same logic is used for both
if (format.design === ArticleDesign.Gallery) {
return insertAdPlaceholdersForGallery(elements);
}
if (
renderingTarget === 'Apps' &&
format.design !== ArticleDesign.LiveBlog &&
format.design !== ArticleDesign.DeadBlog
) {
return insertAdPlaceholders(elements);
}
return elements;
};