diff --git a/src/app/components/PersonalisedContent/index.test.tsx b/src/app/components/PersonalisedContent/index.test.tsx new file mode 100644 index 00000000000..9a9fd84641e --- /dev/null +++ b/src/app/components/PersonalisedContent/index.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { render, screen } from '../react-testing-library-with-providers'; +import PersonalisedContent from '.'; + +const mockPersonalisedContent = [ + { + title: 'Personalised Title', + summaries: [ + { + type: 'promo', + title: 'Promo Title', + description: 'Promo Description', + link: '/promo-link', + imageUrl: 'promo-image.jpg', + imageAlt: 'Promo Image', + isLive: false, + }, + ], + id: 'personalised-content', + link: '/personalised-link', + isFirstCuration: true, + topicId: 'topic-1', + }, +]; + +const basePageData = { + secondaryColumn: { + PersonalisedContent: mockPersonalisedContent, + topStories: [], + features: [], + }, +}; + +describe('PersonalisedContent', () => { + it('renders nothing if there is no personalised content data', () => { + render( + , + ); + expect(screen.queryByRole('region')).not.toBeInTheDocument(); + }); + + it('renders personalised content when variant is "personalised"', () => { + render( + , + ); + expect(screen.getByRole('region')).toHaveAttribute( + 'aria-labelledby', + 'personalised-content', + ); + expect(screen.getByText('Personalised Title')).toBeInTheDocument(); + expect(screen.getByText('Promo Title')).toBeInTheDocument(); + }); + + it('renders the subheading as a link if link is provided', () => { + render( + , + ); + const subheadingLink = screen.getByRole('link', { + name: 'Personalised Title', + }); + expect(subheadingLink).toHaveAttribute('href', '/personalised-link'); + }); +}); diff --git a/src/app/components/PersonalisedContent/index.tsx b/src/app/components/PersonalisedContent/index.tsx new file mode 100644 index 00000000000..b3ac5e599ec --- /dev/null +++ b/src/app/components/PersonalisedContent/index.tsx @@ -0,0 +1,101 @@ +/** @jsx jsx */ +import { jsx } from '@emotion/react'; + +import { Article } from '#app/models/types/optimo'; +import CurationGrid from '#app/components/Curation/CurationGrid'; +import Subheading from '#app/components/Curation/Subhead'; +import { Summary } from '#app/models/types/curationData'; +import { EventTrackingData } from '#app/lib/analyticsUtils/types'; +import useViewTracker from '#app/hooks/useViewTracker'; +import useClickTrackerHandler from '#app/hooks/useClickTrackerHandler'; +import styles from '#app/components/RelatedContentSection/index.styles'; + +const PersonalisedContent = ({ + pageData, + personalisedTopicCurationExperimentVariant, +}: { + pageData: Article; + personalisedTopicCurationExperimentVariant: string; +}) => { + type PersonalisedContentType = { + title?: string; + summaries?: Summary[]; + curationLength?: number; + id?: string; + link?: string; + renderVisuallyHiddenH2Title?: boolean; + curationSubheading?: string; + isFirstCuration?: boolean; + topicId?: string; + }; + + const personalisedContentArray = pageData.secondaryColumn + ?.PersonalisedContent as PersonalisedContentType[] | undefined; + + const getPersonalisedContentData = () => { + if (personalisedTopicCurationExperimentVariant !== 'personalised') { + return undefined; + } + if ( + !Array.isArray(personalisedContentArray) || + personalisedContentArray.length === 0 + ) { + return undefined; + } + // Country-specific data is always first + return personalisedContentArray[0]; + }; + + const personalisedContentData = getPersonalisedContentData(); + const { + title, + summaries = [], + id = 'personalised-content', + link = '', + isFirstCuration = false, + topicId = '', + } = personalisedContentData || {}; + + const eventTrackingData: EventTrackingData = { + componentName: 'personalised-topic-curation', + sendOptimizelyEvents: true, + experimentName: 'newswb_ws_location_based_topics', + experimentVariant: personalisedTopicCurationExperimentVariant, + groupTracker: { + name: title, + type: 'personalised-topic-curation', + ...(link && { link }), + ...(topicId && { resourceId: topicId }), + ...(summaries?.length > 0 && { itemCount: summaries.length }), + }, + }; + const viewTracker = useViewTracker(eventTrackingData); + const subheadingClickTracker = useClickTrackerHandler(eventTrackingData); + + if (!personalisedContentData) { + return null; + } + + return ( +
+ {title && ( + + {title} + + )} + +
+ ); +}; + +export default PersonalisedContent; diff --git a/src/app/models/types/optimo.ts b/src/app/models/types/optimo.ts index 19b8fd3de51..8f24a59541b 100644 --- a/src/app/models/types/optimo.ts +++ b/src/app/models/types/optimo.ts @@ -135,6 +135,7 @@ export type SecondaryColumn = { mediaCuration?: Curation; topStories: TopStoryItem[]; features: object[]; + PersonalisedContent?: object[]; latestMedia?: LatestMedia[]; }; diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx index 050ef04bf8f..f63dbc86ba8 100644 --- a/src/app/pages/ArticlePage/ArticlePage.tsx +++ b/src/app/pages/ArticlePage/ArticlePage.tsx @@ -49,6 +49,7 @@ import { Recommendation } from '#app/models/types/onwardJourney'; import ScrollablePromo from '#components/ScrollablePromo'; import Recommendations from '#app/components/Recommendations'; import { ReadTimeArticleExperiment as ReadTime } from '#app/components/ReadTime'; +import PersonalisedContent from '../../components/PersonalisedContent'; import ElectionBanner from './ElectionBanner'; import ImageWithCaption from '../../components/ImageWithCaption'; import AdContainer from '../../components/Ad'; @@ -246,6 +247,27 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { experimentType: ExperimentType.CLIENT_SIDE, }); + // EXPERIMENT: Personalised Content Rail + const personalisedContentExperimentName = 'newswb_ws_location_based_topics'; + let personalisedContentExperimentVariant = useOptimizelyVariation({ + experimentName: personalisedContentExperimentName, + experimentType: ExperimentType.CLIENT_SIDE, + }); + + // temp overrides for testing + const personalisedContentOverride = true; + personalisedContentExperimentVariant = 'personalised'; + + const showPersonalisedContent = personalisedContentOverride + ? true + : Boolean( + !isAmp && + !isLite && + !isApp && + personalisedContentExperimentVariant && + personalisedContentExperimentVariant === 'personalised', + ); + const allowAdvertising = pageData?.metadata?.allowAdvertising ?? false; const adcampaign = pageData?.metadata?.adCampaignKeyword; @@ -470,6 +492,15 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { }, })} /> + {/* EXPERIMENT: Personalised Content */} + {showPersonalisedContent && ( + + )} {!isApp && !isPGL && ( { }, ); }); + + describe('Personalised topic curation', () => { + it('renders nothing if personalisedContentData is undefined', () => { + const pageData = { + ...articleDataNews, + secondaryColumn: { + topStories: [], + features: [], + }, + }; + const { container } = render( + , + ); + expect(container).toBeEmptyDOMElement(); + }); + it('renders section and subheading when personalisedContentData is present', () => { + const personalisedContent = [ + { + title: 'Recommended for you', + summaries: [ + { + type: 'promo', + title: 'Article 1', + description: 'Description 1', + link: '/article-1', + imageUrl: 'image-1.jpg', + imageAlt: 'Image 1', + isLive: false, + id: 'article-1', + }, + { + type: 'promo', + title: 'Article 2', + description: 'Description 2', + link: '/article-2', + imageUrl: 'image-2.jpg', + imageAlt: 'Image 2', + isLive: false, + id: 'article-2', + }, + ], + id: 'personalised-content', + topicId: 'topic-1', + }, + ]; + const pageData = { + ...articleDataNews, + secondaryColumn: { + topStories: [], + features: [], + PersonalisedContent: personalisedContent, + }, + }; + render( + , + ); + expect(screen.getByRole('region')).toBeInTheDocument(); + expect(screen.getByText('Recommended for you')).toBeInTheDocument(); + expect(screen.getByText('Article 1')).toBeInTheDocument(); + expect(screen.getByText('Article 2')).toBeInTheDocument(); + }); + }); }); diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts index 458d4418ec7..cf6ec0a0d85 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts @@ -13,6 +13,8 @@ import augmentWithDisclaimer from '#app/routes/article/utils/augmentWithDisclaim import shouldRender from '#app/legacy/containers/PageHandlers/withData/shouldRender'; import { ArticleMetadata } from '#app/models/types/optimo'; import { getServerExperiments } from '#server/utilities/experimentHeader'; +import fetchDataFromBFF from '#app/routes/utils/fetchDataFromBFF'; +import getAgent from '#server/utilities/getAgent'; import getPageData from '../../../utilities/pageRequests/getPageData'; const logger = nodeLogger(__filename); @@ -93,8 +95,14 @@ export default async (context: GetServerSidePropsContext) => { throw handleError('Article data is malformed', 500); } - const { article, secondaryData } = data?.pageData || {}; + const country = (reqHeaders['x-country'] || reqHeaders['x-bbc-edge-country']) + ?.toString() + .toLowerCase(); + + const shouldAttemptPersonalisedTopicExperience = + service === 'mundo' && !isAmp && Boolean(country); + const { article, secondaryData } = data?.pageData || {}; const isArticleOlderThanSixHours = Date.now() - article.metadata.lastPublished > 21600000; const maxAge = isArticleOlderThanSixHours ? 90 : 45; @@ -111,7 +119,62 @@ export default async (context: GetServerSidePropsContext) => { mostRead = null, billboardCuration = null, mediaCuration = null, - } = secondaryData; + } = secondaryData || {}; + + let personalisedContent; + + if (shouldAttemptPersonalisedTopicExperience) { + const countrySpecificTopics: Record = { + ar: 'c7zp57yy6dzt', + cl: 'c340qyppkk8t', + mx: 'c340qyp6yggt', + co: 'c404v5gz1rkt', + es: 'c6vzy3wd189t', + ve: 'cpzd49v9rd1t', + us: 'cdr5613yzwqt', + uy: 'cpzd498zwj6t', + do: 'cr50y7pykkdt', + }; + + const countrySpecificId = + country && countrySpecificTopics[country.toString()]; + + if (countrySpecificId) { + try { + const countrySpecificData = await fetchDataFromBFF({ + pathname: `/${service}/topics/${countrySpecificId}?renderer_env=live`, + pageType: 'topic', + service, + variant: variant || undefined, + isAmp, + getAgent, + }); + + const countryArticles = + countrySpecificData?.json?.data?.curations?.[0]?.summaries || []; + + if (countrySpecificData?.json?.data) { + personalisedContent = [ + { + title: countrySpecificData.json.data.title, + description: countrySpecificData.json.data.description, + link: `/${service}/topics/${countrySpecificId}`, + summaries: Array.isArray(countryArticles) + ? countryArticles.slice(0, 4) + : [], + topicId: countrySpecificId, + }, + ]; + } + } catch (error) { + logger.warn('PERSONALISED_CONTENT_TOPIC_FETCH_FAILED', { + message: (error as Error)?.message, + country, + topicId: countrySpecificId, + }); + } + } + } const transformedArticleData = transformPageData(toggles)(article); @@ -145,6 +208,9 @@ export default async (context: GetServerSidePropsContext) => { latestMedia, mediaCuration, billboardCuration, + ...(personalisedContent && { + PersonalisedContent: personalisedContent, + }), }, mostRead, },