diff --git a/ab-testing/config/abTests.ts b/ab-testing/config/abTests.ts
index 5a71df2c826..9fb9d0a837c 100644
--- a/ab-testing/config/abTests.ts
+++ b/ab-testing/config/abTests.ts
@@ -70,6 +70,18 @@ const ABTests: ABTest[] = [
groups: ["control", "variant"],
shouldForceMetricsCollection: false,
},
+ {
+ name: "fronts-and-curation-personalised-container",
+ description: "Testing the a personalised container component on fronts",
+ owners: ["fronts.and.curation@guardian.co.uk"],
+ expirationDate: `2026-02-22`,
+ type: "server",
+ status: "ON",
+ audienceSize: 0 / 100,
+ audienceSpace: "A",
+ groups: ["control", "variant"],
+ shouldForceMetricsCollection: false,
+ },
];
const activeABtests = ABTests.filter((test) => test.status === "ON");
diff --git a/dotcom-rendering/src/components/Card/Card.tsx b/dotcom-rendering/src/components/Card/Card.tsx
index 67ac1787560..151d1df0d40 100644
--- a/dotcom-rendering/src/components/Card/Card.tsx
+++ b/dotcom-rendering/src/components/Card/Card.tsx
@@ -168,6 +168,7 @@ export type Props = {
isInStarRatingVariant?: boolean;
starRatingSize?: RatingSizeType;
isInOnwardsAbTestVariant?: boolean;
+ isInPersonalisationVariant?: boolean;
};
const starWrapper = (cardHasImage: boolean) => css`
@@ -201,13 +202,16 @@ const waveformWrapper = (
left: 0;
right: 0;
bottom: 0;
+
svg {
display: block;
width: 100%;
height: ${mediaPositionOnMobile === 'top' ? 50 : 29}px;
+
${from.mobileMedium} {
height: ${mediaPositionOnMobile === 'top' ? 50 : 33}px;
}
+
${from.tablet} {
height: ${mediaPositionOnDesktop === 'top' ? 50 : 33}px;
}
@@ -221,12 +225,15 @@ const HorizontalDivider = () => (
border-top: 1px solid ${palette('--card-border-top')};
height: 1px;
width: 50%;
+
${from.tablet} {
width: 100px;
}
+
${from.desktop} {
width: 140px;
}
+
margin-top: ${space[3]}px;
}
`}
@@ -241,6 +248,7 @@ const podcastImageStyles = (
return css`
width: 69px;
height: 69px;
+
${from.tablet} {
width: 98px;
height: 98px;
@@ -254,11 +262,14 @@ const podcastImageStyles = (
return css`
width: 98px;
height: 98px;
+
${from.tablet} {
width: 120px;
height: 120px;
}
+
/** The image takes the full height on desktop, so that the waveform sticks to the bottom of the card. */
+
${from.desktop} {
width: ${isHorizontalOnDesktop ? 'unset' : '120px'};
height: ${isHorizontalOnDesktop ? 'unset' : '120px'};
@@ -429,6 +440,7 @@ export const Card = ({
isInStarRatingVariant,
starRatingSize = 'small',
isInOnwardsAbTestVariant,
+ isInPersonalisationVariant,
}: Props) => {
const hasSublinks = supportingContent && supportingContent.length > 0;
const sublinkPosition = decideSublinkPosition(
@@ -612,8 +624,8 @@ export const Card = ({
/**
- * Media cards have contrasting background colours. We add additional
- * padding to these cards to keep the text readable.
-- */
+* padding to these cards to keep the text readable.
+- */
const isMediaCardOrNewsletter = isMediaCard(format) || isNewsletter;
const showPill = isMediaCardOrNewsletter && !isGallerySecondaryOnward;
@@ -910,6 +922,7 @@ export const Card = ({
${until.tablet} {
display: none;
}
+
${from.desktop} {
display: none;
}
@@ -946,6 +959,7 @@ export const Card = ({
headlineText={headlineText}
dataLinkName={resolvedDataLinkName}
isExternalLink={isExternalLink}
+ isInPersonalisationVariant={isInPersonalisationVariant}
/>
{headlinePosition === 'outer' && (
void;
}) => {
return (
// eslint-disable-next-line jsx-a11y/anchor-has-content -- we have an aria-label attribute describing the content
@@ -39,6 +43,7 @@ const InternalLink = ({
css={fauxLinkStyles}
data-link-name={dataLinkName}
aria-label={headlineText}
+ onClick={trackPersonalisationCardClick}
/>
);
};
@@ -47,12 +52,12 @@ const ExternalLink = ({
linkTo,
headlineText,
dataLinkName,
- trackCardClick,
+ trackPersonalisationCardClick,
}: {
linkTo: string;
headlineText: string;
dataLinkName?: string;
- trackCardClick?: () => void;
+ trackPersonalisationCardClick?: () => void;
}) => {
return (
// eslint-disable-next-line jsx-a11y/anchor-has-content -- we have an aria-label attribute describing the content
@@ -63,7 +68,7 @@ const ExternalLink = ({
aria-label={headlineText + ' (opens in new tab)'}
target="_blank"
rel="noreferrer"
- onClick={trackCardClick}
+ onClick={trackPersonalisationCardClick}
/>
);
};
@@ -73,6 +78,7 @@ export const CardLink = ({
headlineText,
dataLinkName = 'article', //this makes sense if the link is to an article, but should this say something like "external" if it's an external link? are there any other uses/alternatives?
isExternalLink,
+ isInPersonalisationVariant,
}: Props) => {
return (
<>
@@ -81,6 +87,10 @@ export const CardLink = ({
linkTo={linkTo}
headlineText={headlineText}
dataLinkName={dataLinkName}
+ trackPersonalisationCardClick={() =>
+ isInPersonalisationVariant &&
+ trackPersonalisationClick(linkTo)
+ }
/>
)}
{!isExternalLink && (
@@ -88,6 +98,10 @@ export const CardLink = ({
linkTo={linkTo}
headlineText={headlineText}
dataLinkName={dataLinkName}
+ trackPersonalisationCardClick={() =>
+ isInPersonalisationVariant &&
+ trackPersonalisationClick(linkTo)
+ }
/>
)}
>
diff --git a/dotcom-rendering/src/components/Card/components/LI.tsx b/dotcom-rendering/src/components/Card/components/LI.tsx
index 1657bea3f19..b34c773a886 100644
--- a/dotcom-rendering/src/components/Card/components/LI.tsx
+++ b/dotcom-rendering/src/components/Card/components/LI.tsx
@@ -100,6 +100,8 @@ type Props = {
offsetBottomPaddingOnDivider?: boolean;
/** Overrides the vertical divider colour */
verticalDividerColour?: string;
+
+ isVisible?: boolean;
};
export const LI = ({
@@ -114,6 +116,7 @@ export const LI = ({
snapAlignStart = false,
offsetBottomPaddingOnDivider = false,
verticalDividerColour = palette('--section-border'),
+ isVisible = true,
}: Props) => {
// Decide sizing
const sizeStyles = decideSize(percentage, stretch);
@@ -133,6 +136,7 @@ export const LI = ({
padSidesOnMobile &&
sidePaddingStylesMobile(padSidesMobileOverride),
snapAlignStart && snapAlignStartStyles,
+ { visibility: isVisible ? 'visible' : 'hidden' },
]}
>
{children}
diff --git a/dotcom-rendering/src/components/DecideContainer.tsx b/dotcom-rendering/src/components/DecideContainer.tsx
index 5ff6ed3bed9..2011045087c 100644
--- a/dotcom-rendering/src/components/DecideContainer.tsx
+++ b/dotcom-rendering/src/components/DecideContainer.tsx
@@ -6,6 +6,7 @@ import type {
DCRContainerType,
DCRFrontCard,
DCRGroupedTrails,
+ PillarBucket,
} from '../types/front';
import { DynamicFast } from './DynamicFast';
import { DynamicPackage } from './DynamicPackage';
@@ -27,6 +28,7 @@ import { FlexibleGeneral } from './FlexibleGeneral';
import { FlexibleSpecial } from './FlexibleSpecial';
import { Island } from './Island';
import { NavList } from './NavList';
+import { PersonalisedMediumFour } from './PersonalisedMediumFour.importable';
import { ScrollableFeature } from './ScrollableFeature.importable';
import { ScrollableHighlights } from './ScrollableHighlights.importable';
import { ScrollableMedium } from './ScrollableMedium.importable';
@@ -48,6 +50,7 @@ type Props = {
collectionId: number;
containerLevel?: DCRContainerLevel;
isInStarRatingVariant?: boolean;
+ pillarBuckets?: PillarBucket;
};
export const DecideContainer = ({
@@ -64,6 +67,7 @@ export const DecideContainer = ({
collectionId,
containerLevel,
isInStarRatingVariant,
+ pillarBuckets,
}: Props) => {
switch (containerType) {
case 'dynamic/fast':
@@ -298,6 +302,22 @@ export const DecideContainer = ({
);
case 'static/medium/4':
+ if (pillarBuckets) {
+ return (
+
+
+
+ );
+ }
return (
{
+ if (isMediaCard(format) || isNewsletter) {
+ return 'top';
+ }
+
+ return 'bottom';
+};
+
+type Props = {
+ trails: DCRFrontCard[];
+ imageLoading: Loading;
+ containerPalette?: DCRContainerPalette;
+ showAge?: boolean;
+ serverTime?: number;
+ showImage?: boolean;
+ aspectRatio: AspectRatio;
+ containerLevel?: DCRContainerLevel;
+ isInStarRatingVariant?: boolean;
+ pillarBuckets?: PillarBucket;
+};
+
+const filterBuckets = (
+ pillarBuckets: PillarBucket,
+ demotedCards: string[],
+): DCRPillarCards => {
+ const filteredPillarBuckets: DCRPillarCards = {
+ opinion: [],
+ sport: [],
+ culture: [],
+ lifestyle: [],
+ };
+
+ for (const pillar of PILLARS) {
+ filteredPillarBuckets[pillar] =
+ pillarBuckets[pillar]?.filter(
+ (card) => !demotedCards.includes(card.url),
+ ) ?? [];
+ }
+
+ return filteredPillarBuckets;
+};
+
+export const PersonalisedMediumFour = ({
+ trails,
+ containerPalette,
+ showAge,
+ serverTime,
+ imageLoading,
+ showImage = true,
+ aspectRatio,
+ containerLevel = 'Primary',
+ isInStarRatingVariant,
+ pillarBuckets,
+}: Props) => {
+ const [orderedTrails, setOrderedTrails] = useState(
+ trails.slice(0, 4),
+ );
+ const [shouldShowCards, setShouldShowCards] = useState(false);
+
+ const [hasTrackedView, setHasTrackedView] = useState(false);
+
+ const abTests = useBetaAB();
+ const isInPersonalisationVariant =
+ abTests?.isUserInTestGroup(
+ 'fronts-and-curation-personalised-container',
+ 'variant',
+ ) ?? false;
+
+ useEffect(() => {
+ if (isUndefined(pillarBuckets) || !isInPersonalisationVariant) {
+ setShouldShowCards(true);
+ return;
+ }
+
+ const demotedCards = getDemotedState() ?? [];
+
+ if (demotedCards.length === 0) {
+ setShouldShowCards(true);
+ return;
+ }
+
+ const filteredBuckets = filterBuckets(pillarBuckets, demotedCards);
+
+ const curatedTrails = getCuratedList(filteredBuckets);
+
+ if (curatedTrails.length > 0) {
+ setOrderedTrails(curatedTrails);
+ }
+
+ setShouldShowCards(true);
+ }, [trails, pillarBuckets, isInPersonalisationVariant]);
+
+ useEffect(() => {
+ if (shouldShowCards && !hasTrackedView && isInPersonalisationVariant) {
+ trackView(orderedTrails);
+ setHasTrackedView(true);
+ }
+ }, [
+ orderedTrails,
+ hasTrackedView,
+ shouldShowCards,
+ isInPersonalisationVariant,
+ ]);
+
+ return (
+ <>
+
+ {orderedTrails.map((card, cardIndex) => {
+ return (
+ - 0}
+ isVisible={shouldShowCards}
+ >
+
+
+ );
+ })}
+
+ >
+ );
+};
diff --git a/dotcom-rendering/src/frontend/feFront.ts b/dotcom-rendering/src/frontend/feFront.ts
index e4a7cc3df2c..0675232c780 100644
--- a/dotcom-rendering/src/frontend/feFront.ts
+++ b/dotcom-rendering/src/frontend/feFront.ts
@@ -328,6 +328,7 @@ export type FECollection = {
config: FECollectionConfig;
hasMore: boolean;
targetedTerritory?: Territory;
+ bucket?: FEFrontCard[];
};
export type FEFrontConfig = {
diff --git a/dotcom-rendering/src/layouts/FrontLayout.tsx b/dotcom-rendering/src/layouts/FrontLayout.tsx
index af5744c02bb..7cad0c56338 100644
--- a/dotcom-rendering/src/layouts/FrontLayout.tsx
+++ b/dotcom-rendering/src/layouts/FrontLayout.tsx
@@ -530,6 +530,7 @@ export const FrontLayout = ({ front, NAV }: Props) => {
isInStarRatingVariant={
isInStarRatingVariant
}
+ pillarBuckets={collection.bucket}
/>
diff --git a/dotcom-rendering/src/lib/personalisationHistory.ts b/dotcom-rendering/src/lib/personalisationHistory.ts
new file mode 100644
index 00000000000..1e0e1c6ca07
--- /dev/null
+++ b/dotcom-rendering/src/lib/personalisationHistory.ts
@@ -0,0 +1,126 @@
+import { isObject, storage } from '@guardian/libs';
+import type { DCRFrontCard } from '../types/front';
+
+export const DemotedCardsHistoryKey = 'gu.history.demotedCards';
+export const ViewedCardsHistoryKey = 'gu.history.viewedCards';
+
+const MAX_VIEW_COUNT = 2;
+
+type DemotedCardHistory = string[];
+type CardHistory = { cardUrl: string; viewCount: number };
+type ViewedCardHistory = CardHistory[];
+
+/**
+ *
+ * A card qualifies for demotion if it has been either
+ * clicked once
+ * or
+ * viewed twice
+ *
+ * We use these two metrics as a proxy for (positive or negative) user engagement with the card
+ */
+
+const isValidDemotedState = (history: unknown): history is DemotedCardHistory =>
+ Array.isArray(history) &&
+ history.every((cardUrl) => typeof cardUrl === 'string');
+
+/* Retrieve the user's demoted card list from local storage */
+export const getDemotedState = (): DemotedCardHistory | undefined => {
+ try {
+ const demotedCardHistory = storage.local.get(DemotedCardsHistoryKey);
+
+ if (!isValidDemotedState(demotedCardHistory)) {
+ throw new Error(`Invalid ${DemotedCardsHistoryKey} value`);
+ }
+
+ return demotedCardHistory;
+ } catch (e) {
+ /* error parsing the string, so remove the key */
+ storage.local.remove(DemotedCardsHistoryKey);
+ return undefined;
+ }
+};
+
+const isValidViewState = (history: unknown): history is ViewedCardHistory =>
+ Array.isArray(history) &&
+ history.every(
+ (viewedCard) =>
+ isObject(viewedCard) &&
+ 'cardUrl' in viewedCard &&
+ 'viewCount' in viewedCard &&
+ typeof viewedCard.cardUrl === 'string' &&
+ typeof viewedCard.viewCount === 'number',
+ );
+
+/* Retrieve the user's viewed card state from local storage */
+export const getViewedState = (): ViewedCardHistory | undefined => {
+ try {
+ const ViewedCardState = storage.local.get(ViewedCardsHistoryKey);
+
+ if (!isValidViewState(ViewedCardState)) {
+ throw new Error(`Invalid ${ViewedCardsHistoryKey} value`);
+ }
+
+ return ViewedCardState;
+ } catch (e) {
+ /* error parsing the string, so remove the key */
+ storage.local.remove(ViewedCardsHistoryKey);
+ return undefined;
+ }
+};
+
+export const trackView = (cards: DCRFrontCard[]): void => {
+ const recentlyViewedCardUrls = cards.map((card) => card.url);
+
+ const viewedCards = getViewedState() ?? [];
+
+ const cardsForDemotion: DemotedCardHistory = [];
+
+ const updatedViewedCards = [...viewedCards];
+
+ for (const url of recentlyViewedCardUrls) {
+ const index = updatedViewedCards.findIndex(
+ (card) => card.cardUrl === url,
+ );
+
+ if (index > -1 && updatedViewedCards[index]) {
+ // Card already exists, increment count by 1
+ const incrementedViewCount =
+ updatedViewedCards[index].viewCount + 1;
+
+ const updatedCard = {
+ ...updatedViewedCards[index],
+ viewCount: incrementedViewCount,
+ };
+
+ updatedViewedCards[index] = updatedCard;
+
+ if (incrementedViewCount >= MAX_VIEW_COUNT) {
+ cardsForDemotion.push(updatedCard.cardUrl);
+ }
+ } else {
+ // New card -> add with count 1
+ updatedViewedCards.push({
+ cardUrl: url,
+ viewCount: 1,
+ });
+ }
+ }
+
+ // Persist viewed cards
+ storage.local.set(ViewedCardsHistoryKey, updatedViewedCards);
+
+ // Persist deduped list of demoted cards
+ if (cardsForDemotion.length > 0) {
+ const demotedCards = getDemotedState() ?? [];
+ const nextDemoted = Array.from(
+ new Set([...demotedCards, ...cardsForDemotion]),
+ );
+
+ storage.local.set(DemotedCardsHistoryKey, nextDemoted);
+ }
+};
+export const trackPersonalisationClick = (url: string): void => {
+ const demotedCards = getDemotedState() ?? [];
+ storage.local.set(DemotedCardsHistoryKey, [...demotedCards, url]);
+};
diff --git a/dotcom-rendering/src/model/createCollection.ts b/dotcom-rendering/src/model/createCollection.ts
new file mode 100644
index 00000000000..44468c8e477
--- /dev/null
+++ b/dotcom-rendering/src/model/createCollection.ts
@@ -0,0 +1,140 @@
+import type { FECollection, FEFrontCard } from '../frontend/feFront';
+import type { DCRCollectionType, DCRFrontCard } from '../types/front';
+import { enhanceCards } from './enhanceCards';
+
+const personalisedCollection: DCRCollectionType = {
+ id: 'hardcoded-collection',
+ displayName: 'Across The Guardian',
+ description: undefined,
+ collectionType: 'static/medium/4',
+ href: undefined,
+ grouped: {
+ splash: [],
+ snap: [],
+ huge: [],
+ veryBig: [],
+ big: [],
+ standard: [],
+ },
+ curated: [],
+ backfill: [],
+ treats: [],
+ config: {
+ showDateHeader: false,
+ },
+ canShowMore: false,
+ targetedTerritory: undefined,
+ aspectRatio: '5:4',
+};
+
+export type Pillar = 'opinion' | 'sport' | 'culture' | 'lifestyle';
+
+export const PILLARS = [
+ 'opinion',
+ 'sport',
+ 'culture',
+ 'lifestyle',
+] as const satisfies readonly Pillar[];
+
+type PillarContainer = {
+ pillar: Pillar;
+ containerName: string;
+};
+const pillarContainers: PillarContainer[] = [
+ //culture
+ { containerName: 'Culture', pillar: 'culture' },
+ { containerName: 'What to watch', pillar: 'culture' },
+ { containerName: 'What to listen to', pillar: 'culture' },
+ { containerName: 'What to read', pillar: 'culture' },
+ { containerName: 'What to play', pillar: 'culture' },
+ { containerName: 'What to visit', pillar: 'culture' },
+ { containerName: 'More culture', pillar: 'culture' },
+ //opinion
+ { containerName: 'Opinion', pillar: 'opinion' },
+ { containerName: 'More opinion', pillar: 'opinion' },
+ { containerName: 'Editorials', pillar: 'opinion' },
+ //sport
+ { containerName: 'Sport', pillar: 'sport' },
+ { containerName: 'More sport', pillar: 'sport' },
+ //lifestyle
+ { containerName: 'Lifestyle', pillar: 'lifestyle' },
+ { containerName: 'The Filter', pillar: 'lifestyle' },
+ { containerName: 'Food', pillar: 'lifestyle' },
+ { containerName: 'Relationships', pillar: 'lifestyle' },
+ { containerName: 'Money & consumer', pillar: 'lifestyle' },
+ { containerName: 'Health & fitness', pillar: 'lifestyle' },
+ { containerName: 'Fashion & beauty', pillar: 'lifestyle' },
+ { containerName: 'Travel', pillar: 'lifestyle' },
+ { containerName: 'More Lifestyle', pillar: 'lifestyle' },
+];
+
+export type DCRPillarCards = Record;
+
+const getPillarCards = (collections: FECollection[]): DCRPillarCards => {
+ const HighlightUrls = collections
+ .filter((collection) => 'Highlights' === collection.displayName)
+ .flatMap((collection) => collection.curated)
+ .map((card) => card.properties.webUrl);
+
+ const pillarCards: Record = {
+ lifestyle: [],
+ opinion: [],
+ sport: [],
+ culture: [],
+ };
+
+ for (const collection of collections) {
+ const pillarContainer = pillarContainers.find(
+ (pillar) => pillar.containerName === collection.displayName,
+ );
+
+ if (!pillarContainer) continue;
+ const curatedCards = [...collection.curated].filter(
+ (card) => !HighlightUrls.includes(card.properties.webUrl),
+ );
+
+ pillarCards[pillarContainer.pillar].push(...curatedCards);
+ }
+
+ return {
+ lifestyle: enhanceCards(pillarCards.lifestyle, {
+ cardInTagPage: false,
+ discussionApiUrl: 'string',
+ editionId: 'UK',
+ }),
+ opinion: enhanceCards(pillarCards.opinion, {
+ cardInTagPage: false,
+ discussionApiUrl: 'string',
+ editionId: 'UK',
+ }),
+ sport: enhanceCards(pillarCards.sport, {
+ cardInTagPage: false,
+ discussionApiUrl: 'string',
+ editionId: 'UK',
+ }),
+ culture: enhanceCards(pillarCards.culture, {
+ cardInTagPage: false,
+ discussionApiUrl: 'string',
+ editionId: 'UK',
+ }),
+ };
+};
+
+export const getCuratedList = (buckets: DCRPillarCards): DCRFrontCard[] => {
+ return PILLARS.map((pillar) => buckets[pillar][0]).filter(
+ (card): card is DCRFrontCard => card !== undefined,
+ );
+};
+
+export const createFakeCollection = (
+ collections: FECollection[],
+): DCRCollectionType => {
+ const pillarCards = getPillarCards(collections);
+ const curatedList = getCuratedList(pillarCards);
+
+ return {
+ ...personalisedCollection,
+ curated: curatedList,
+ bucket: pillarCards,
+ };
+};
diff --git a/dotcom-rendering/src/server/handler.front.web.ts b/dotcom-rendering/src/server/handler.front.web.ts
index c338f2101f3..b439933c0f4 100644
--- a/dotcom-rendering/src/server/handler.front.web.ts
+++ b/dotcom-rendering/src/server/handler.front.web.ts
@@ -4,6 +4,7 @@ import type { FEFront } from '../frontend/feFront';
import type { FETagPage } from '../frontend/feTagPage';
import { decideTagPageBranding, pickBrandingForEdition } from '../lib/branding';
import { decideTrail } from '../lib/decideTrail';
+import { createFakeCollection } from '../model/createCollection';
import { enhanceCards } from '../model/enhanceCards';
import { enhanceCollections } from '../model/enhanceCollections';
import {
@@ -24,6 +25,41 @@ const enhanceFront = (body: unknown): Front => {
const serverTime = Date.now();
+ const isInPersonalisedContainerTest =
+ data.config.serverSideABTests[
+ `fronts-and-curation-personalised-container`
+ ];
+
+ const personalisedContainer = isInPersonalisedContainerTest
+ ? createFakeCollection(data.pressedPage.collections)
+ : undefined;
+
+ const collections = enhanceCollections({
+ collections: data.pressedPage.collections,
+ editionId: data.editionId,
+ pageId: data.pageId,
+ onPageDescription: data.pressedPage.frontProperties.onPageDescription,
+ isOnPaidContentFront: data.config.isPaidContent,
+ discussionApiUrl: data.config.discussionApiUrl,
+ frontBranding: pickBrandingForEdition(
+ data.pressedPage.frontProperties.commercial.editionBrandings,
+ data.editionId,
+ ),
+ });
+
+ const personalisedContainerPosition =
+ data.pressedPage.collections.findIndex(
+ (c) => c.displayName === 'News',
+ ) + 1;
+
+ const combinedCollections = personalisedContainer
+ ? [
+ ...collections.slice(0, personalisedContainerPosition),
+ personalisedContainer,
+ ...collections.slice(personalisedContainerPosition),
+ ]
+ : collections;
+
return {
...data,
webTitle: `${
@@ -31,20 +67,7 @@ const enhanceFront = (body: unknown): Front => {
} | The Guardian`,
pressedPage: {
...data.pressedPage,
- collections: enhanceCollections({
- collections: data.pressedPage.collections,
- editionId: data.editionId,
- pageId: data.pageId,
- onPageDescription:
- data.pressedPage.frontProperties.onPageDescription,
- isOnPaidContentFront: data.config.isPaidContent,
- discussionApiUrl: data.config.discussionApiUrl,
- frontBranding: pickBrandingForEdition(
- data.pressedPage.frontProperties.commercial
- .editionBrandings,
- data.editionId,
- ),
- }),
+ collections: combinedCollections,
},
mostViewed: data.mostViewed.map((trail) => decideTrail(trail)),
trendingTopics: extractTrendingTopicsFomFront(
diff --git a/dotcom-rendering/src/types/front.ts b/dotcom-rendering/src/types/front.ts
index 50269bbc989..d024c405cd9 100644
--- a/dotcom-rendering/src/types/front.ts
+++ b/dotcom-rendering/src/types/front.ts
@@ -117,7 +117,9 @@ export type DCRSnapType = {
};
export type AspectRatio = FEAspectRatio;
-
+export type PillarBucket = {
+ [key: string]: DCRFrontCard[];
+};
export type DCRCollectionType = {
id: string;
displayName: string;
@@ -144,6 +146,7 @@ export type DCRCollectionType = {
collectionBranding?: CollectionBranding;
targetedTerritory?: Territory;
aspectRatio?: AspectRatio;
+ bucket?: PillarBucket;
};
export type DCRGroupedTrails = {