From c274b8ace509a0b4dd2ffe60c9c6fe04c8ec806a Mon Sep 17 00:00:00 2001 From: Olly <9575458+OllysCoding@users.noreply.github.com> Date: Wed, 5 Jul 2023 10:55:49 +0100 Subject: [PATCH] Ads for Tag Fronts & respect speed (#8056) * feat: Add support for tuples Co-Authored-By: Max Duval * feat: Add support for fast & slow containers on tag fronts * feat: Inject MPU states into containers on Tag fronts Revert "feat: Inject MPU states into containers on Tag fronts" This reverts commit 4d8140dd3ae5e31ebbaa340b332862e1cf5c68ab. Revert "Revert "feat: Inject MPU states into containers on Tag fronts"" This reverts commit 7bca0a66bd23c90d7a4b1c129c72959245d16e51. * WIP: Add support for MPU injection & ads in tag fronts * feat: Add support for desktop & mobile ads on tag fronts * Update slow MPU containers to match frontend for tag fronts * use containerId for key on tag fronts map * Add stories for mpu containers on tag fronts & fix any bugs * Add stories for DecideContainerByTrails & fix layout bugs * refactor: tidier definitions for tuples Co-Authored-By: Max Duval * refactor: Use exhaustive checks for tag front MPU components Co-Authored-By: Max Duval * fix: remove unused import * feat: Introduce 'TupleOrLess`and `takeFirst` Co-Authored-By: Max Duval * refactor: Use exhaustive checking for DecideContainerByTrails Co-Authored-By: Max Duval * fix: dont render empty ULs if theres only 8 cards * Add tests for tuples takeFirst * chore: Update comment for takeFirst * fix: use correct %'s on DecideContainerByTrails * fix: remove exports from tag front mpu containers as they're not used * Update dotcom-rendering/src/lib/getAdPositions.ts Co-authored-by: Max Duval * Update dotcom-rendering/src/lib/getAdPositions.ts Co-authored-by: Max Duval * Merge branch 'olly/tag-fronts-containers-and-ads' of github.com:guardian/dotcom-rendering into olly/tag-fronts-containers-and-ads * chore: remove isTupleOrGreater as this is no longer used * refactor: use takeFirst in injectMpuIntoGroupedTrails * Final tidy-up --------- Co-authored-by: Max Duval Co-authored-by: Max Duval --- .../DecideContainerByTrails.stories.tsx | 188 ++++++++++ .../components/DecideContainerByTrails.tsx | 323 +++++++++++++----- .../components/TagFrontFastMpu.stories.tsx | 103 ++++++ .../src/components/TagFrontFastMpu.tsx | 184 ++++++++++ .../components/TagFrontSlowMpu.stories.tsx | 94 +++++ .../src/components/TagFrontSlowMpu.tsx | 165 +++++++++ dotcom-rendering/src/layouts/FrontLayout.tsx | 2 +- .../src/layouts/TagFrontLayout.tsx | 118 +++++-- dotcom-rendering/src/lib/getAdPositions.ts | 30 ++ dotcom-rendering/src/lib/tuple.test.ts | 83 +++++ dotcom-rendering/src/lib/tuple.ts | 84 +++++ .../src/model/groupTrailsByDates.ts | 11 +- .../src/model/injectMpuIntoGroupedTrails.ts | 81 +++++ .../src/server/index.front.web.ts | 13 +- dotcom-rendering/src/types/tagFront.ts | 34 +- 15 files changed, 1385 insertions(+), 128 deletions(-) create mode 100644 dotcom-rendering/src/components/DecideContainerByTrails.stories.tsx create mode 100644 dotcom-rendering/src/components/TagFrontFastMpu.stories.tsx create mode 100644 dotcom-rendering/src/components/TagFrontFastMpu.tsx create mode 100644 dotcom-rendering/src/components/TagFrontSlowMpu.stories.tsx create mode 100644 dotcom-rendering/src/components/TagFrontSlowMpu.tsx create mode 100644 dotcom-rendering/src/lib/tuple.test.ts create mode 100644 dotcom-rendering/src/lib/tuple.ts create mode 100644 dotcom-rendering/src/model/injectMpuIntoGroupedTrails.ts diff --git a/dotcom-rendering/src/components/DecideContainerByTrails.stories.tsx b/dotcom-rendering/src/components/DecideContainerByTrails.stories.tsx new file mode 100644 index 00000000000..b0b49b3a477 --- /dev/null +++ b/dotcom-rendering/src/components/DecideContainerByTrails.stories.tsx @@ -0,0 +1,188 @@ +import { breakpoints } from '@guardian/source-foundations'; +import { trails } from '../../fixtures/manual/trails'; +import { DecideContainerByTrails } from './DecideContainerByTrails'; +import { FrontSection } from './FrontSection'; + +export default { + component: DecideContainerByTrails, + title: 'Components/DecideContainerByTrails', + parameters: { + chromatic: { + viewports: [ + breakpoints.mobile, + breakpoints.tablet, + breakpoints.wide, + ], + }, + }, +}; + +export const OneCardFast = () => { + return ( + + + + ); +}; +OneCardFast.storyName = 'Fast - One card'; + +export const TwoCardFast = () => { + return ( + + + + ); +}; +TwoCardFast.storyName = 'Fast - Two cards'; + +export const ThreeCardFast = () => { + return ( + + + + ); +}; +ThreeCardFast.storyName = 'Fast - Three cards'; + +export const FourCardFast = () => { + return ( + + + + ); +}; +FourCardFast.storyName = 'Fast - Four cards'; + +export const FiveCardFast = () => { + return ( + + + + ); +}; +FiveCardFast.storyName = 'Fast - Five cards'; + +export const SixCardFast = () => { + return ( + + + + ); +}; +SixCardFast.storyName = 'Fast - Six cards'; + +export const SevenCardFast = () => { + return ( + + + + ); +}; +SevenCardFast.storyName = 'Fast - Seven cards'; + +export const EightCardFast = () => { + return ( + + + + ); +}; + +EightCardFast.storyName = 'Fast - Eight cards'; + +export const TwelveCardFast = () => { + return ( + + + + ); +}; +TwelveCardFast.storyName = 'Fast - Twelve cards'; + +export const OneCardSlow = () => { + return ( + + + + ); +}; +OneCardSlow.storyName = 'Slow - One card'; + +export const TwoCardSlow = () => { + return ( + + + + ); +}; +TwoCardSlow.storyName = 'Slow - Two cards'; + +export const ThreeCardSlow = () => { + return ( + + + + ); +}; +ThreeCardSlow.storyName = 'Slow - Three cards'; + +export const FourCardSlow = () => { + return ( + + + + ); +}; +FourCardSlow.storyName = 'Slow - Four cards'; + +export const FiveCardSlow = () => { + return ( + + + + ); +}; +FiveCardSlow.storyName = 'Slow - Five cards'; + +export const SixCardSlow = () => { + return ( + + + + ); +}; +SixCardSlow.storyName = 'Slow - Six cards'; + +export const SevenCardSlow = () => { + return ( + + + + ); +}; +SevenCardSlow.storyName = 'Slow - Seven cards'; + +export const EightCardSlow = () => { + return ( + + + + ); +}; + +EightCardSlow.storyName = 'Slow - Eight cards'; + +export const TwelveCardSlow = () => { + return ( + + + + ); +}; +TwelveCardSlow.storyName = 'Slow - Twelve cards'; diff --git a/dotcom-rendering/src/components/DecideContainerByTrails.tsx b/dotcom-rendering/src/components/DecideContainerByTrails.tsx index 12a3cb1cd78..7ea8691ae52 100644 --- a/dotcom-rendering/src/components/DecideContainerByTrails.tsx +++ b/dotcom-rendering/src/components/DecideContainerByTrails.tsx @@ -1,10 +1,13 @@ import { Card100Media50, + Card100Media75, Card25Media25, Card33Media33, Card50Media50, CardDefault, } from '../lib/cardWrappers'; +import type { Tuple } from '../lib/tuple'; +import { takeFirst } from '../lib/tuple'; import type { DCRFrontCard } from '../types/front'; import { LI } from './Card/components/LI'; import { UL } from './Card/components/UL'; @@ -24,8 +27,17 @@ export const OneCardFast = ({ trail }: { trail: DCRFrontCard }) => { ); }; -export const TwoCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { - if (!trails[0] || !trails[1]) return <>; +export const OneCardSlow = ({ trail }: { trail: DCRFrontCard }) => { + return ( +
    +
  • + +
  • +
+ ); +}; + +export const TwoCard = ({ trails }: { trails: Tuple }) => { return (
  • @@ -38,8 +50,7 @@ export const TwoCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { ); }; -export const ThreeCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { - if (!trails[0] || !trails[1] || !trails[2]) return <>; +export const ThreeCard = ({ trails }: { trails: Tuple }) => { return (
    • @@ -55,8 +66,7 @@ export const ThreeCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { ); }; -export const FourCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { - if (!trails[0] || !trails[1] || !trails[2] || !trails[3]) return <>; +export const FourCard = ({ trails }: { trails: Tuple }) => { return (
      • @@ -75,9 +85,11 @@ export const FourCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { ); }; -export const FiveCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { - if (!trails[0] || !trails[1] || !trails[2] || !trails[3] || !trails[4]) - return <>; +export const FiveCardFast = ({ + trails, +}: { + trails: Tuple; +}) => { return (
        • @@ -103,17 +115,37 @@ export const FiveCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { ); }; -export const SixCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { - if ( - !trails[0] || - !trails[1] || - !trails[2] || - !trails[3] || - !trails[4] || - !trails[5] - ) - return <>; +export const FiveCardSlow = ({ + trails, +}: { + trails: Tuple; +}) => { + return ( + <> +
            +
          • + +
          • +
          • + +
          • +
          +
            +
          • + +
          • +
          • + +
          • +
          • + +
          • +
          + + ); +}; +export const SixCardFast = ({ trails }: { trails: Tuple }) => { return ( <>
            @@ -141,18 +173,41 @@ export const SixCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { ); }; -export const SevenCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { - if ( - !trails[0] || - !trails[1] || - !trails[2] || - !trails[3] || - !trails[4] || - !trails[5] || - !trails[6] - ) - return <>; +export const SixCardSlow = ({ trails }: { trails: Tuple }) => { + return ( + <> +
              +
            • + +
            • +
            • + +
            • +
            • + +
            • +
            +
              +
            • + +
            • +
            • + +
            • +
            • + +
            • +
            + + ); +}; + +export const SevenCardFast = ({ + trails, +}: { + trails: Tuple; +}) => { return ( <>
              @@ -184,18 +239,48 @@ export const SevenCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { ); }; -export const EightCardFast = ({ trails }: { trails: DCRFrontCard[] }) => { - if ( - !trails[0] || - !trails[1] || - !trails[2] || - !trails[3] || - !trails[4] || - !trails[5] || - !trails[6] || - !trails[7] - ) - return <>; +export const SevenCardSlow = ({ + trails, +}: { + trails: Tuple; +}) => { + return ( + <> +
                +
              • + +
              • +
              • + +
              • +
              • + +
              • +
              +
                +
              • + +
              • +
              • + +
              • +
              • + +
              • +
              • + +
              • +
              + + ); +}; + +export const EightOrMoreFast = ({ + trails, +}: { + trails: [...Tuple, ...DCRFrontCard[]]; +}) => { + const afterEight = trails.slice(8); return ( <> @@ -213,7 +298,7 @@ export const EightCardFast = ({ trails }: { trails: DCRFrontCard[] }) => {
            -
              +
                0}>
              • @@ -227,55 +312,135 @@ export const EightCardFast = ({ trails }: { trails: DCRFrontCard[] }) => {
              + {afterEight.length > 0 ? ( +
                + {afterEight.map((trail, index) => ( +
              • + +
              • + ))} +
              + ) : ( + <> + )} ); }; -export const BeyondEight = ({ trails }: { trails: DCRFrontCard[] }) => { +export const EightOrMoreSlow = ({ + trails, +}: { + trails: [...Tuple, ...DCRFrontCard[]]; +}) => { const afterEight = trails.slice(8); return ( <> - ; -
                - {afterEight.map((trail, index) => ( -
              • - -
              • - ))} +
                  +
                • + +
                • +
                • + +
                • +
                • + +
                • +
                • + +
                • +
                +
                  0}> +
                • + +
                • +
                • + +
                • +
                • + +
                • +
                • + +
                + {afterEight.length > 0 ? ( +
                  + {afterEight.map((trail, index) => ( +
                • + +
                • + ))} +
                + ) : ( + <> + )} ); }; -export const DecideContainerByTrails = ({ trails }: Props) => { - // TODO: Respect 'speed' +export const DecideContainerByTrails = ({ trails, speed }: Props) => { + const initialTrails = takeFirst(trails, 8); - switch (trails.length) { - case 1: - return trails[0] !== undefined ? ( - - ) : null; - case 2: - return ; - case 3: - return ; - case 4: - return ; - case 5: - return ; - case 6: - return ; - case 7: - return ; - case 8: - return ; - default: - return ; + if (speed === 'fast') { + switch (initialTrails.length) { + case 0: + return <>; + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + case 6: + return ; + case 7: + return ; + case 8: + return ( + + ); + } + } else { + switch (initialTrails.length) { + case 0: + return <>; + case 1: + return ; + case 2: + return ; + case 3: + return ; + case 4: + return ; + case 5: + return ; + case 6: + return ; + case 7: + return ; + case 8: + return ( + + ); + } } }; diff --git a/dotcom-rendering/src/components/TagFrontFastMpu.stories.tsx b/dotcom-rendering/src/components/TagFrontFastMpu.stories.tsx new file mode 100644 index 00000000000..d9eba85511b --- /dev/null +++ b/dotcom-rendering/src/components/TagFrontFastMpu.stories.tsx @@ -0,0 +1,103 @@ +import { breakpoints } from '@guardian/source-foundations'; +import { trails } from '../../fixtures/manual/trails'; +import { FrontSection } from './FrontSection'; +import { TagFrontFastMpu } from './TagFrontFastMpu'; + +export default { + component: TagFrontFastMpu, + title: 'Components/TagFrontFastMpu', + parameters: { + chromatic: { + viewports: [ + breakpoints.mobile, + breakpoints.tablet, + breakpoints.wide, + ], + }, + }, +}; + +export const WithTwoCards = () => { + return ( + + + + ); +}; +WithTwoCards.storyName = 'With two cards'; + +export const WithFourCards = () => { + return ( + + + + ); +}; +WithFourCards.storyName = 'With four cards'; + +export const WithSixCards = () => { + return ( + + + + ); +}; +WithSixCards.storyName = 'With six cards'; + +export const WithNineCards = () => { + return ( + + + + ); +}; +WithNineCards.storyName = 'With nine cards'; diff --git a/dotcom-rendering/src/components/TagFrontFastMpu.tsx b/dotcom-rendering/src/components/TagFrontFastMpu.tsx new file mode 100644 index 00000000000..261279da0c8 --- /dev/null +++ b/dotcom-rendering/src/components/TagFrontFastMpu.tsx @@ -0,0 +1,184 @@ +import { Hide } from '@guardian/source-react-components'; +import { Card33Media33, CardDefault } from '../lib/cardWrappers'; +import { type Tuple } from '../lib/tuple'; +import type { DCRFrontCard } from '../types/front'; +import type { GroupedTrailsFastMpu } from '../types/tagFront'; +import { AdSlot } from './AdSlot'; +import { LI } from './Card/components/LI'; +import { UL } from './Card/components/UL'; + +const TwoCard = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( +
                  +
                • + +
                • +
                • + +
                • +
                • + + + +
                • +
                + ); +}; + +const FourCard = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( +
                  +
                • + +
                • +
                • +
                    +
                  • + +
                  • +
                  • + +
                  • +
                  • + +
                  • +
                  +
                • +
                • + + + +
                • +
                + ); +}; + +const SixCard = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( +
                  +
                • +
                    +
                  • + +
                  • +
                  • + +
                  • +
                  • + +
                  • +
                  +
                • +
                • +
                    +
                  • + +
                  • +
                  • + +
                  • +
                  • + +
                  • +
                  +
                • +
                • + + + +
                • +
                + ); +}; + +const NineCard = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( + <> +
                  +
                • + +
                • +
                • + +
                • +
                • + +
                • +
                +
                  +
                • +
                    +
                  • + +
                  • +
                  • + +
                  • +
                  • + +
                  • +
                  +
                • +
                • +
                    +
                  • + +
                  • +
                  • + +
                  • +
                  • + +
                  • +
                  +
                • +
                • + + + +
                • +
                + + ); +}; + +type Props = GroupedTrailsFastMpu & { + adIndex: number; +}; + +export const TagFrontFastMpu = ({ trails, adIndex }: Props) => { + switch (trails.length) { + case 2: + return ; + case 4: + return ; + case 6: + return ; + case 9: + return ; + } +}; diff --git a/dotcom-rendering/src/components/TagFrontSlowMpu.stories.tsx b/dotcom-rendering/src/components/TagFrontSlowMpu.stories.tsx new file mode 100644 index 00000000000..f023b9c17c0 --- /dev/null +++ b/dotcom-rendering/src/components/TagFrontSlowMpu.stories.tsx @@ -0,0 +1,94 @@ +import { breakpoints } from '@guardian/source-foundations'; +import { trails } from '../../fixtures/manual/trails'; +import { FrontSection } from './FrontSection'; +import { TagFrontSlowMpu } from './TagFrontSlowMpu'; + +export default { + component: TagFrontSlowMpu, + title: 'Components/TagFrontSlowMpu', + parameters: { + chromatic: { + viewports: [ + breakpoints.mobile, + breakpoints.tablet, + breakpoints.wide, + ], + }, + }, +}; + +export const WithTwoCards = () => { + return ( + + + + ); +}; +WithTwoCards.storyName = 'With two cards'; + +export const WithFourCards = () => { + return ( + + + + ); +}; +WithFourCards.storyName = 'With four cards'; + +export const WithFiveCards = () => { + return ( + + + + ); +}; +WithFiveCards.storyName = 'With five cards'; + +export const WithSevenCards = () => { + return ( + + + + ); +}; +WithSevenCards.storyName = 'With seven cards'; diff --git a/dotcom-rendering/src/components/TagFrontSlowMpu.tsx b/dotcom-rendering/src/components/TagFrontSlowMpu.tsx new file mode 100644 index 00000000000..616818f1763 --- /dev/null +++ b/dotcom-rendering/src/components/TagFrontSlowMpu.tsx @@ -0,0 +1,165 @@ +import { Hide } from '@guardian/source-react-components'; +import { Card33Media33, Card50Media50, CardDefault } from '../lib/cardWrappers'; +import { type Tuple } from '../lib/tuple'; +import type { DCRFrontCard } from '../types/front'; +import type { GroupedTrailsSlowMpu } from '../types/tagFront'; +import { AdSlot } from './AdSlot'; +import { LI } from './Card/components/LI'; +import { UL } from './Card/components/UL'; + +const TwoCard = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( +
                  +
                • + +
                • +
                • + +
                • +
                • + + + +
                • +
                + ); +}; + +const FourCard = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( +
                  +
                • + +
                • +
                • +
                    +
                  • + +
                  • +
                  • + +
                  • +
                  • + +
                  • +
                  +
                • +
                • + + + +
                • +
                + ); +}; + +const FiveCard = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( + <> +
                  +
                • + +
                • +
                • + +
                • +
                • + +
                • +
                +
                  +
                • + +
                • +
                • + +
                • +
                • + + + +
                • +
                + + ); +}; + +const SevenCards = ({ + trails, + adIndex, +}: { + trails: Tuple; + adIndex: number; +}) => { + return ( + <> +
                  +
                • + +
                • +
                • + +
                • +
                +
                  +
                • + +
                • +
                • + +
                • +
                • + +
                • +
                +
                  +
                • + +
                • +
                • + +
                • +
                • + + + +
                • +
                + + ); +}; + +type Props = GroupedTrailsSlowMpu & { + adIndex: number; +}; + +export const TagFrontSlowMpu = ({ trails, adIndex }: Props) => { + switch (trails.length) { + case 2: + return ; + case 4: + return ; + case 5: + return ; + case 7: + return ; + } +}; diff --git a/dotcom-rendering/src/layouts/FrontLayout.tsx b/dotcom-rendering/src/layouts/FrontLayout.tsx index b969ce58e6e..f737062ff79 100644 --- a/dotcom-rendering/src/layouts/FrontLayout.tsx +++ b/dotcom-rendering/src/layouts/FrontLayout.tsx @@ -75,7 +75,7 @@ const isToggleable = ( } else return index != 0 && !isNavList(collection); }; -const decideAdSlot = ( +export const decideAdSlot = ( renderAds: boolean, index: number, isNetworkFront: boolean | undefined, diff --git a/dotcom-rendering/src/layouts/TagFrontLayout.tsx b/dotcom-rendering/src/layouts/TagFrontLayout.tsx index 4fb7dd97394..35d3b6d9150 100644 --- a/dotcom-rendering/src/layouts/TagFrontLayout.tsx +++ b/dotcom-rendering/src/layouts/TagFrontLayout.tsx @@ -8,6 +8,7 @@ import { news, } from '@guardian/source-foundations'; import { StraightLines } from '@guardian/source-react-components-development-kitchen'; +import { Fragment } from 'react'; import { AdSlot } from '../components/AdSlot'; import { DecideContainerByTrails } from '../components/DecideContainerByTrails'; import { Footer } from '../components/Footer'; @@ -18,13 +19,20 @@ import { Island } from '../components/Island'; import { Nav } from '../components/Nav/Nav'; import { Section } from '../components/Section'; import { SubNav } from '../components/SubNav.importable'; +import { TagFrontFastMpu } from '../components/TagFrontFastMpu'; import { TagFrontHeader } from '../components/TagFrontHeader'; +import { TagFrontSlowMpu } from '../components/TagFrontSlowMpu'; import { TrendingTopics } from '../components/TrendingTopics'; import { canRenderAds } from '../lib/canRenderAds'; import { decidePalette } from '../lib/decidePalette'; import { getEditionFromId } from '../lib/edition'; +import { + getMerchHighPosition, + getTagFrontMobileAdPositions, +} from '../lib/getAdPositions'; import type { NavType } from '../model/extract-nav'; import type { DCRTagFrontType } from '../types/tagFront'; +import { decideAdSlot } from './FrontLayout'; import { Stuck } from './lib/stickiness'; interface Props { @@ -66,11 +74,23 @@ export const TagFrontLayout = ({ tagFront, NAV }: Props) => { const palette = decidePalette(format); + const merchHighPosition = getMerchHighPosition( + tagFront.groupedTrails.length, + false, + ); + /** * This property currently only applies to the header and merchandising slots */ const renderAds = canRenderAds(tagFront); + const mobileAdPositions = renderAds + ? getTagFrontMobileAdPositions( + tagFront.groupedTrails, + merchHighPosition, + ) + : []; + return ( <>
                @@ -194,6 +214,35 @@ export const TagFrontLayout = ({ tagFront, NAV }: Props) => { groupedTrails.day !== undefined, ); + const ContainerComponent = () => { + if ( + 'injected' in groupedTrails && + 'speed' in groupedTrails + ) { + if (groupedTrails.speed === 'fast') { + return ( + + ); + } else { + return ( + + ); + } + } + return ( + + ); + }; + const url = groupedTrails.day !== undefined ? `/${tagFront.pageId}/${groupedTrails.year}/${date @@ -210,37 +259,44 @@ export const TagFrontLayout = ({ tagFront, NAV }: Props) => { : undefined; return ( - - - + + + + + {decideAdSlot( + renderAds, + index, + false, + tagFront.groupedTrails.length, + tagFront.config.isPaidContent, + mobileAdPositions, + tagFront.config.hasPageSkin, + )} + ); })} diff --git a/dotcom-rendering/src/lib/getAdPositions.ts b/dotcom-rendering/src/lib/getAdPositions.ts index cebb1a0ff02..005a09d87d4 100644 --- a/dotcom-rendering/src/lib/getAdPositions.ts +++ b/dotcom-rendering/src/lib/getAdPositions.ts @@ -1,4 +1,5 @@ import type { DCRCollectionType } from '../types/front'; +import type { GroupedTrailsBase } from '../types/tagFront'; type AdCandidate = Pick; @@ -74,6 +75,35 @@ export const getMobileAdPositions = ( // Should insert no more than 10 ads .slice(0, 10); +/** + * Uses a very similar approach to pressed fronts, except we + * - Do not need to consider thrashers + * - Do not need to consider the 'most viewed' container + * + * The types are also slightly different, as we no longer have + * specific container IDs, so we use the date which is unique + */ +export const getTagFrontMobileAdPositions = ( + collections: Array, + merchHighPosition: number, +): number[] => + collections + .filter( + (_, index) => + !hasAdjacentCommercialContainer(index, merchHighPosition), + ) + .filter(isEvenIndex) + .map((collection) => + collections.findIndex( + ({ day, month, year }) => + day === collection.day && + month === collection.month && + year === collection.year, + ), + ) + // Should insert no more than 10 ads + .slice(0, 10); + const hasDesktopAd = (collection: AdCandidate) => { return ( collection.collectionType == 'dynamic/slow-mpu' || diff --git a/dotcom-rendering/src/lib/tuple.test.ts b/dotcom-rendering/src/lib/tuple.test.ts new file mode 100644 index 00000000000..8751a04161c --- /dev/null +++ b/dotcom-rendering/src/lib/tuple.test.ts @@ -0,0 +1,83 @@ +import { takeFirst } from './tuple'; + +describe('takeFirst', () => { + it('Always returns the correct array length when the array is one less, the same as, or one more than n', () => { + const results = [ + // Format, from 1 to 12, cover length n - 1, n & n+1 + takeFirst([0, 1], 1), + takeFirst([0], 1), + takeFirst([], 1), + takeFirst([0, 1, 2], 2), + takeFirst([0, 1], 2), + takeFirst([0], 2), + takeFirst([0, 1, 2, 3], 3), + takeFirst([0, 1, 2], 3), + takeFirst([0, 1], 3), + takeFirst([0, 1, 2, 3, 4], 4), + takeFirst([0, 1, 2, 3], 4), + takeFirst([0, 1, 2], 4), + takeFirst([0, 1, 2, 3, 4, 5], 5), + takeFirst([0, 1, 2, 3, 4], 5), + takeFirst([0, 1, 2, 3], 5), + takeFirst([0, 1, 2, 3, 4, 5, 6], 6), + takeFirst([0, 1, 2, 3, 4, 5], 6), + takeFirst([0, 1, 2, 3, 4], 6), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7], 7), + takeFirst([0, 1, 2, 3, 4, 5, 6], 7), + takeFirst([0, 1, 2, 3, 4, 5], 7), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8], 8), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7], 8), + takeFirst([0, 1, 2, 3, 4, 5, 6], 8), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8], 9), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7], 9), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 10), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8], 10), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 11), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 11), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 11), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 12), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 12), + takeFirst([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 12), + ] as const; + + // Expected results from n are n, n & n-1 + expect(results[0].length).toEqual(1); + expect(results[1].length).toEqual(1); + expect(results[2].length).toEqual(0); + expect(results[3].length).toEqual(2); + expect(results[4].length).toEqual(2); + expect(results[5].length).toEqual(1); + expect(results[6].length).toEqual(3); + expect(results[7].length).toEqual(3); + expect(results[8].length).toEqual(2); + expect(results[9].length).toEqual(4); + expect(results[10].length).toEqual(4); + expect(results[11].length).toEqual(3); + expect(results[12].length).toEqual(5); + expect(results[13].length).toEqual(5); + expect(results[14].length).toEqual(4); + expect(results[15].length).toEqual(6); + expect(results[16].length).toEqual(6); + expect(results[17].length).toEqual(5); + expect(results[18].length).toEqual(7); + expect(results[19].length).toEqual(7); + expect(results[20].length).toEqual(6); + expect(results[21].length).toEqual(8); + expect(results[22].length).toEqual(8); + expect(results[23].length).toEqual(7); + expect(results[24].length).toEqual(9); + expect(results[25].length).toEqual(9); + expect(results[26].length).toEqual(8); + expect(results[27].length).toEqual(10); + expect(results[28].length).toEqual(10); + expect(results[29].length).toEqual(9); + expect(results[30].length).toEqual(11); + expect(results[31].length).toEqual(11); + expect(results[32].length).toEqual(10); + expect(results[33].length).toEqual(12); + expect(results[34].length).toEqual(12); + expect(results[35].length).toEqual(11); + }); +}); diff --git a/dotcom-rendering/src/lib/tuple.ts b/dotcom-rendering/src/lib/tuple.ts new file mode 100644 index 00000000000..a93956eab63 --- /dev/null +++ b/dotcom-rendering/src/lib/tuple.ts @@ -0,0 +1,84 @@ +/** A tuple of up to 12 items. Larger tuples will not be narrowed */ +export type Tuple = N extends 12 + ? [T, T, T, T, T, T, T, T, T, T, T, T] + : N extends 11 + ? [T, T, T, T, T, T, T, T, T, T, T] + : N extends 10 + ? [T, T, T, T, T, T, T, T, T, T] + : N extends 9 + ? [T, T, T, T, T, T, T, T, T] + : N extends 8 + ? [T, T, T, T, T, T, T, T] + : N extends 7 + ? [T, T, T, T, T, T, T] + : N extends 6 + ? [T, T, T, T, T, T] + : N extends 5 + ? [T, T, T, T, T] + : N extends 4 + ? [T, T, T, T] + : N extends 3 + ? [T, T, T] + : N extends 2 + ? [T, T] + : N extends 1 + ? [T] + : N extends 0 + ? [] + : T[]; + +/** + * Type-guard for whether an array is a tuple of exact length. + * + * Only tuples of 12 elements or less will be narrowed. + */ +export const isTuple = ( + arr: Array | ReadonlyArray, + count: N, +): arr is Tuple => arr.length === count; + +/** Type where a tuple can have any 'n' number of items or less */ +type SlicedTuple< + T, + N extends 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12, +> = N extends 12 + ? Tuple + : N extends 11 + ? Tuple + : N extends 10 + ? Tuple + : N extends 9 + ? Tuple + : N extends 8 + ? Tuple + : N extends 7 + ? Tuple + : N extends 6 + ? Tuple + : N extends 5 + ? Tuple + : N extends 4 + ? Tuple + : N extends 3 + ? Tuple + : N extends 2 + ? Tuple + : N extends 1 + ? Tuple + : undefined; + +/** + * Takes the first 'n' number of items in an array + * + * By returning `SlicedTuple` you receive a type-safe response + * that can be checked exhaustively. + */ +export const takeFirst = < + T, + N extends 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12, +>( + array: Array | ReadonlyArray, + count: N, +): SlicedTuple => + //@ts-expect-error – this output is tested by jest and it’s a very helpful method + array.slice(0, count); diff --git a/dotcom-rendering/src/model/groupTrailsByDates.ts b/dotcom-rendering/src/model/groupTrailsByDates.ts index b750df0a998..687fe2b1567 100644 --- a/dotcom-rendering/src/model/groupTrailsByDates.ts +++ b/dotcom-rendering/src/model/groupTrailsByDates.ts @@ -1,4 +1,5 @@ import type { DCRFrontCard } from '../types/front'; +import type { GroupedTrails } from '../types/tagFront'; /** * The number of trails per day required (on average) for trails to be @@ -15,16 +16,6 @@ interface TrailAndDate { date: Date; } -/** - * Represents a set of trails grouped by their year, month & optionally day of publication. - */ -export interface GroupedTrails { - year: number; - month: number; - day: number | undefined; - trails: DCRFrontCard[]; -} - /** * Calculates the average number of trails per day across a set of days */ diff --git a/dotcom-rendering/src/model/injectMpuIntoGroupedTrails.ts b/dotcom-rendering/src/model/injectMpuIntoGroupedTrails.ts new file mode 100644 index 00000000000..f8753ed2bfe --- /dev/null +++ b/dotcom-rendering/src/model/injectMpuIntoGroupedTrails.ts @@ -0,0 +1,81 @@ +import { takeFirst } from '../lib/tuple'; +import type { + GroupedTrails, + GroupedTrailsFastMpu, + GroupedTrailsSlowMpu, +} from '../types/tagFront'; + +/** + * Injects an MPU container into a list of grouped trails + * + * For both slow & fast tag fronts, containers of certain lengths can receive an MPU slot. + * The code looks for the first container of the right length and injects the ad slot + * object (GroupedTrailsSlowMpu / GroupedTrailsFastMpu). + */ +export const injectMpuIntoGroupedTrails = ( + groupedTrails: GroupedTrails[], + speed: 'slow' | 'fast', +): Array => { + let injected = false; + const result: Array< + GroupedTrails | GroupedTrailsFastMpu | GroupedTrailsSlowMpu + > = []; + + groupedTrails.forEach((grouped) => { + if (injected) { + result.push(grouped); + return; + } + + if (speed === 'fast') { + // When we have a container with > 9 trails for fast, + // we 'cap' the number of trails at 9 in order to fit an MPU in. + // Containers that don't get an MPU injected will of course still be + // able to show more than 9 trails. + const fastTrails = takeFirst(grouped.trails, 9); + switch (fastTrails.length) { + case 2: + case 4: + case 6: + case 9: + injected = true; + result.push({ + day: grouped.day, + month: grouped.month, + year: grouped.year, + trails: fastTrails, + injected: true, + speed: 'fast', + }); + break; + default: + break; + } + } else { + // By taking the first 12, we get the benefit of being able to use + // a switch statement here, without 'capping' the number of trails in the + // same way we do when 'fast' + const slowTrails = takeFirst(grouped.trails, 12); + switch (slowTrails.length) { + case 2: + case 4: + case 5: + case 7: + injected = true; + result.push({ + day: grouped.day, + month: grouped.month, + year: grouped.year, + trails: slowTrails, + injected: true, + speed: 'slow', + }); + break; + default: + break; + } + } + }); + + return result; +}; diff --git a/dotcom-rendering/src/server/index.front.web.ts b/dotcom-rendering/src/server/index.front.web.ts index 36f97e26980..559a6428af8 100644 --- a/dotcom-rendering/src/server/index.front.web.ts +++ b/dotcom-rendering/src/server/index.front.web.ts @@ -7,6 +7,7 @@ import { extractTrendingTopicsFomFront, } from '../model/extractTrendingTopics'; import { groupTrailsByDates } from '../model/groupTrailsByDates'; +import { injectMpuIntoGroupedTrails } from '../model/injectMpuIntoGroupedTrails'; import { getSpeedFromTrails } from '../model/slowOrFastByTrails'; import { validateAsFrontType, validateAsTagFrontType } from '../model/validate'; import { recordTypeAndPlatform } from '../server/lib/logging-store'; @@ -54,13 +55,17 @@ const enhanceTagFront = (body: unknown): DCRTagFrontType => { }); const speed = getSpeedFromTrails(data.contents); + const groupedTrails = groupTrailsByDates( + enhancedCards, + speed === 'slow' || data.forceDay, + ); + return { ...data, tags: data.tags.tags, - groupedTrails: groupTrailsByDates( - enhancedCards, - speed === 'slow' || data.forceDay, - ), + groupedTrails: data.isAdFreeUser + ? groupedTrails + : injectMpuIntoGroupedTrails(groupedTrails, speed), speed, // Pagination information comes from the first tag pagination: diff --git a/dotcom-rendering/src/types/tagFront.ts b/dotcom-rendering/src/types/tagFront.ts index d0cff6ac6ce..5b177db2949 100644 --- a/dotcom-rendering/src/types/tagFront.ts +++ b/dotcom-rendering/src/types/tagFront.ts @@ -1,7 +1,7 @@ import type { EditionId } from '../lib/edition'; -import type { GroupedTrails } from '../model/groupTrailsByDates'; +import type { Tuple } from '../lib/tuple'; import type { FooterType } from './footer'; -import type { FEFrontCard, FEFrontConfigType } from './front'; +import type { DCRFrontCard, FEFrontCard, FEFrontConfigType } from './front'; import type { FETagType } from './tag'; export interface FETagFrontType { @@ -23,8 +23,36 @@ export interface FETagFrontType { forceDay: boolean; } +/** + * Represents a set of trails grouped by their year, month & optionally day of publication. + */ +export interface GroupedTrailsBase { + year: number; + month: number; + day: number | undefined; +} + +export interface GroupedTrails extends GroupedTrailsBase { + trails: DCRFrontCard[]; +} + +export interface GroupedTrailsFastMpu extends GroupedTrailsBase { + injected: true; + speed: 'fast'; + // Trails must either be length of 2, 4, 6, 9 + trails: Tuple; +} +export interface GroupedTrailsSlowMpu extends GroupedTrailsBase { + injected: true; + speed: 'slow'; + // Trails must either be length of 2, 4, 5, 7 + trails: Tuple; +} + export interface DCRTagFrontType { - groupedTrails: GroupedTrails[]; + groupedTrails: Array< + GroupedTrails | GroupedTrailsFastMpu | GroupedTrailsSlowMpu + >; nav: FENavType; tags: FETagType[]; editionId: EditionId;