diff --git a/dotcom-rendering/src/layouts/GalleryLayout.tsx b/dotcom-rendering/src/layouts/GalleryLayout.tsx index 3079fa02fa3..3debef0ce33 100644 --- a/dotcom-rendering/src/layouts/GalleryLayout.tsx +++ b/dotcom-rendering/src/layouts/GalleryLayout.tsx @@ -7,6 +7,8 @@ import { } from '@guardian/source/foundations'; import { Hide } from '@guardian/source/react-components'; import { Fragment } from 'react'; +import { AdPlaceholder } from '../components/AdPlaceholder.apps'; +import { AdPortals } from '../components/AdPortals.importable'; import { AdSlot } from '../components/AdSlot.web'; import { AppsFooter } from '../components/AppsFooter.importable'; import { ArticleHeadline } from '../components/ArticleHeadline'; @@ -31,7 +33,6 @@ import { type ArticleFormat, ArticleSpecial } from '../lib/articleFormat'; import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { decideMainMediaCaption } from '../lib/decide-caption'; -import { getAdPositions } from '../lib/getGalleryAdPositions'; import type { NavType } from '../model/extract-nav'; import { palette as themePalette } from '../palette'; import type { Gallery } from '../types/article'; @@ -171,10 +172,6 @@ export const GalleryLayout = (props: WebProps | AppProps) => { const contributionsServiceUrl = getContributionsServiceUrl(frontendData); - const adPositions: number[] = renderAds - ? getAdPositions(gallery.images) - : []; - return ( <> {isWeb && ( @@ -231,6 +228,11 @@ export const GalleryLayout = (props: WebProps | AppProps) => { backgroundColor: themePalette('--article-background'), }} > + {isApps && renderAds && ( + + + + )}
{ ) : null}
- {gallery.images.map((element, idx) => { - const index = idx + 1; - const shouldShowAds = adPositions.includes(index); - + {gallery.bodyElements.map((element, index) => { + const isImage = + element._type === + 'model.dotcomrendering.pageElements.ImageBlockElement'; + const shouldShowAds = + element._type === + 'model.dotcomrendering.pageElements.AdPlaceholderBlockElement'; return ( - - - {isWeb && shouldShowAds && ( -
-
- - - - - - -
-
-
+ + {isImage && ( + + )} + {shouldShowAds && renderAds && ( + <> + {isWeb && ( +
+
+ + + + + + +
+
+
+ )} + {isApps && } + )}
); diff --git a/dotcom-rendering/src/lib/getGalleryAdPositions.ts b/dotcom-rendering/src/lib/getGalleryAdPositions.ts deleted file mode 100644 index 5c2980ff9eb..00000000000 --- a/dotcom-rendering/src/lib/getGalleryAdPositions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ImageBlockElement } from '../types/content'; - -/** - * This function calculates the positions in the gallery where ads should be placed. - * The ad position will be the same for all breakpoints, after every 4 images will have an ad slot. - * The logic difference between mobile and desktop slot ids is in GalleryAdSlots.tsx and AdSlot.web.tsx. - * @param images - An array of ImageBlockElement objects representing the images in the gallery. - * @returns An array of numbers representing the positions where ads should be placed. - */ -const getAdPositions = (images: ImageBlockElement[]): number[] => { - const adPositions = images - .map((image) => images.indexOf(image) + 1) - .filter((position) => position % 4 === 0); - return adPositions; -}; - -export { getAdPositions }; diff --git a/dotcom-rendering/src/model/enhance-ad-placeholders.ts b/dotcom-rendering/src/model/enhance-ad-placeholders.ts index 28bee3b4d90..6a1fa57da40 100644 --- a/dotcom-rendering/src/model/enhance-ad-placeholders.ts +++ b/dotcom-rendering/src/model/enhance-ad-placeholders.ts @@ -69,6 +69,63 @@ const insertPlaceholder = ( 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 @@ -140,10 +197,22 @@ export const enhanceAdPlaceholders = renderingTarget: RenderingTarget, shouldHideAds: boolean, ) => - (elements: FEElement[]): FEElement[] => - renderingTarget === 'Apps' && - !shouldHideAds && - format.design !== ArticleDesign.LiveBlog && - format.design !== ArticleDesign.DeadBlog - ? insertAdPlaceholders(elements) - : elements; + (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; + }; diff --git a/dotcom-rendering/src/types/article.ts b/dotcom-rendering/src/types/article.ts index 869d47a3c9a..4a05a9f9dd7 100644 --- a/dotcom-rendering/src/types/article.ts +++ b/dotcom-rendering/src/types/article.ts @@ -18,7 +18,12 @@ import { type TableOfContentsItem, } from '../model/enhanceTableOfContents'; import { enhancePinnedPost } from '../model/pinnedPost'; -import type { FEElement, ImageBlockElement, ImageForLightbox } from './content'; +import type { + AdPlaceholderBlockElement, + FEElement, + ImageBlockElement, + ImageForLightbox, +} from './content'; import { type RenderingTarget } from './renderingTarget'; /** @@ -40,7 +45,7 @@ export type ArticleFields = { export type Gallery = ArticleFields & { design: ArticleDesign.Gallery; - images: ImageBlockElement[]; + bodyElements: (ImageBlockElement | AdPlaceholderBlockElement)[]; mainMedia: ImageBlockElement; }; @@ -130,11 +135,13 @@ export const enhanceArticleType = ( design, display: format.display, theme: format.theme, - images: blocks.flatMap((block) => + bodyElements: blocks.flatMap((block) => block.elements.filter( (element) => element._type === - 'model.dotcomrendering.pageElements.ImageBlockElement', + 'model.dotcomrendering.pageElements.ImageBlockElement' || + element._type === + 'model.dotcomrendering.pageElements.AdPlaceholderBlockElement', ), ), mainMedia: getGalleryMainMedia(