diff --git a/dotcom-rendering/src/components/SportDataPageComponent.tsx b/dotcom-rendering/src/components/SportDataPageComponent.tsx index 5b6fb0497ab..8a99d7dd8d0 100644 --- a/dotcom-rendering/src/components/SportDataPageComponent.tsx +++ b/dotcom-rendering/src/components/SportDataPageComponent.tsx @@ -5,7 +5,7 @@ import { buildAdTargeting } from '../lib/ad-targeting'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; import { rootStyles } from '../lib/rootStyles'; import { filterABTestSwitches } from '../model/enhance-switches'; -import type { SportDataPage } from '../sportDataPage'; +import type { AppSportDataPage, WebSportDataPage } from '../sportDataPage'; import { AlreadyVisited } from './AlreadyVisited.importable'; import { useConfig } from './ConfigContext'; import { DarkModeMessage } from './DarkModeMessage'; @@ -16,9 +16,7 @@ import { SetABTests } from './SetABTests.importable'; import { SetAdTargeting } from './SetAdTargeting.importable'; import { SkipTo } from './SkipTo'; -type Props = { - sportData: SportDataPage; -}; +type Props = AppSportDataPage | WebSportDataPage; /** * @description @@ -26,7 +24,9 @@ type Props = { * * @param {Props} props * */ -export const SportDataPageComponent = ({ sportData }: Props) => { +export const SportDataPageComponent = (props: Props) => { + const { sportData, renderingTarget } = props; + const adTargeting = buildAdTargeting({ isAdFreeUser: sportData.isAdFreeUser, isSensitive: sportData.config.isSensitive, @@ -36,6 +36,9 @@ export const SportDataPageComponent = ({ sportData }: Props) => { adUnit: sportData.config.adUnit, }); + const isWeb = renderingTarget === 'Web'; + const isApps = renderingTarget === 'Apps'; + /* We use this as our "base" or default format */ const format = { display: ArticleDisplay.Standard, @@ -48,51 +51,76 @@ export const SportDataPageComponent = ({ sportData }: Props) => { return ( - - - - - + {isWeb && ( + <> + + + > + )} - - - - - + + + + + + + + + + + + + + {darkModeAvailable && ( + + Dark mode is a work-in-progress. + + You can{' '} + + opt out anytime + {' '} + if anything is unreadable or odd. + )} - pageIsSensitive={sportData.config.isSensitive} - isDev={!!sportData.config.isDev} - serverSideTests={sportData.config.abTests} - serverSideABTests={sportData.config.serverSideABTests} + > + )} + {isWeb && ( + + )} + + {isApps && ( + - - - - - {darkModeAvailable && ( - - Dark mode is a work-in-progress. - - You can{' '} - - opt out anytime - {' '} - if anything is unreadable or odd. - )} - , ); }; diff --git a/dotcom-rendering/src/layouts/SportDataPageLayout.stories.tsx b/dotcom-rendering/src/layouts/SportDataPageLayout.stories.tsx index 7f53eaf8b0f..72924b038cb 100644 --- a/dotcom-rendering/src/layouts/SportDataPageLayout.stories.tsx +++ b/dotcom-rendering/src/layouts/SportDataPageLayout.stories.tsx @@ -24,16 +24,18 @@ export const Results = { guardianBaseURL: 'https://www.theguardian.com', editionId: 'UK', config: footballData.config, - nav: extractNAV(footballData.nav), pageFooter: footballData.pageFooter, contributionsServiceUrl: 'https://contributions.guardianapis.com', isAdFreeUser: false, }, + renderingTarget: 'Web', + nav: extractNAV(footballData.nav), }, } satisfies Story; export const Live = { args: { + ...Results.args, sportData: { ...Results.args.sportData, kind: 'FootballLiveScores', @@ -43,6 +45,7 @@ export const Live = { export const Fixtures = { args: { + ...Results.args, sportData: { ...Results.args.sportData, kind: 'FootballFixtures', @@ -52,6 +55,7 @@ export const Fixtures = { export const Tables = { args: { + ...Results.args, sportData: { ...Results.args.sportData, kind: 'FootballTables', @@ -62,6 +66,7 @@ export const Tables = { export const CricketMatch = { args: { + ...Results.args, sportData: { ...Results.args.sportData, kind: 'CricketMatch', diff --git a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx index 52936c0ffa3..0d54ed9854d 100644 --- a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx +++ b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx @@ -1,5 +1,6 @@ import { palette } from '@guardian/source/foundations'; import { AdSlot } from '../components/AdSlot.web'; +import { AppsFooter } from '../components/AppsFooter.importable'; import { CricketScorecardPage } from '../components/CricketScorecardPage'; import { FootballMatchesPageWrapper } from '../components/FootballMatchesPageWrapper.importable'; import { FootballMatchInfoPage } from '../components/FootballMatchInfoPage'; @@ -15,19 +16,23 @@ import { SubNav } from '../components/SubNav.importable'; import { canRenderAds } from '../lib/canRenderAds'; import { getContributionsServiceUrl } from '../lib/contributions'; import { useBetaAB } from '../lib/useAB'; -import type { SportDataPage } from '../sportDataPage'; +import { palette as themePalette } from '../palette'; +import type { + AppSportDataPage, + SportDataPage, + WebSportDataPage, +} from '../sportDataPage'; +import type { RenderingTarget } from '../types/renderingTarget'; import { BannerWrapper, Stuck } from './lib/stickiness'; -interface Props { - sportData: SportDataPage; -} - const SportsPage = ({ sportData, renderAds, + renderingTarget, }: { sportData: SportDataPage; renderAds: boolean; + renderingTarget: RenderingTarget; }) => { const abTests = useBetaAB(); const isInVariantGroup = @@ -74,7 +79,7 @@ const SportsPage = ({ /> ); case 'FootballMatchSummary': { - if (isInVariantGroup) { + if (isInVariantGroup || renderingTarget === 'Apps') { return ( { +export const SportDataPageLayout = ( + props: AppSportDataPage | WebSportDataPage, +) => { + const sportData = props.sportData; const { - nav, config: { hasSurveyAd }, } = sportData; + const isWeb = props.renderingTarget === 'Web'; + const isApps = props.renderingTarget === 'Apps'; const pageFooter = sportData.pageFooter; const renderAds = canRenderAds(sportData); @@ -104,94 +113,125 @@ export const SportDataPageLayout = ({ sportData }: Props) => { return ( <> - - {renderAds && ( - + {isWeb && ( + + {renderAds && ( + + + + + + )} + + + + )} + + {isWeb && renderAds && hasSurveyAd && } + + + + {isWeb && ( + <> + {props.nav.subNavSections && ( - + + + - - )} - - - - - {renderAds && hasSurveyAd && } + )} - - - {nav.subNavSections && ( - - - + - - + + + + + + + > + )} + {isApps && ( + <> + + + + + + > )} - - - - - - - - - > ); }; diff --git a/dotcom-rendering/src/server/handler.sportDataPage.web.ts b/dotcom-rendering/src/server/handler.sportDataPage.web.ts index 8b9b5b37809..ddb33542f84 100644 --- a/dotcom-rendering/src/server/handler.sportDataPage.web.ts +++ b/dotcom-rendering/src/server/handler.sportDataPage.web.ts @@ -18,6 +18,7 @@ import type { FEFootballMatchListPage } from '../frontend/feFootballMatchListPag import type { FEFootballTablesPage } from '../frontend/feFootballTablesPage'; import { Pillar } from '../lib/articleFormat'; import { safeParseURL } from '../lib/parse'; +import type { NavType } from '../model/extract-nav'; import { extractNAV } from '../model/extract-nav'; import { validateAsCricketMatchPageType, @@ -33,7 +34,9 @@ import type { FootballTablesPage, Region, } from '../sportDataPage'; +import type { FENavType } from '../types/frontend'; import { makePrefetchHeader } from './lib/header'; +import { renderAppsSportPage } from './render.sportDataPage.app'; import { renderSportPage } from './render.sportDataPage.web'; const decideMatchListPageKind = (pageId: string): FootballMatchListPageKind => { @@ -88,6 +91,13 @@ const getNextPageNoJsUrl = (isProd: boolean, nextPageNoJs?: string) => { return `https://code.dev-theguardian.com${nextPageNoJs}`; }; +const parseNav = (nav: FENavType): NavType => { + return { + ...extractNAV(nav), + selectedPillar: Pillar.Sport, + }; +}; + const parseFEFootballMatchList = ( data: FEFootballMatchListPage, ): FootballMatchListPage => { @@ -112,10 +122,6 @@ const parseFEFootballMatchList = ( ), previousPage: data.previousPage, regions: parseFEFootballCompetitionRegions(data.filters), - nav: { - ...extractNAV(data.nav), - selectedPillar: Pillar.Sport, - }, editionId: data.editionId, guardianBaseURL: data.guardianBaseURL, config: data.config, @@ -131,7 +137,10 @@ export const handleFootballMatchListPage: RequestHandler = ({ body }, res) => { validateAsFootballMatchListPage(body); const parsedFootballData = parseFEFootballMatchList(footballDataValidated); - const { html, prefetchScripts } = renderSportPage(parsedFootballData); + const { html, prefetchScripts } = renderSportPage({ + sportData: parsedFootballData, + nav: parseNav(footballDataValidated.nav), + }); res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html); }; @@ -150,10 +159,6 @@ const parseFEFootballTables = ( tables: parsedFootballTables.value, kind: 'FootballTables', regions: parseFEFootballCompetitionRegions(data.filters), - nav: { - ...extractNAV(data.nav), - selectedPillar: Pillar.Sport, - }, editionId: data.editionId, guardianBaseURL: data.guardianBaseURL, config: data.config, @@ -171,7 +176,10 @@ export const handleFootballTablesPage: RequestHandler = ({ body }, res) => { const parsedFootballTableData = parseFEFootballTables( footballTablesPageValidated, ); - const { html, prefetchScripts } = renderSportPage(parsedFootballTableData); + const { html, prefetchScripts } = renderSportPage({ + sportData: parsedFootballTableData, + nav: parseNav(footballTablesPageValidated.nav), + }); res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html); }; @@ -187,10 +195,7 @@ const parseFECricketMatch = (data: FECricketMatchPage): CricketMatchPage => { return { match: parsedCricketMatch.value, kind: 'CricketMatch', - nav: { - ...extractNAV(data.nav), - selectedPillar: Pillar.Sport, - }, + editionId: data.editionId, guardianBaseURL: data.guardianBaseURL, config: data.config, @@ -208,8 +213,10 @@ export const handleCricketMatchPage: RequestHandler = ({ body }, res) => { const parsedCricketMatchData = parseFECricketMatch( cricketMatchPageValidated, ); - - const { html, prefetchScripts } = renderSportPage(parsedCricketMatchData); + const { html, prefetchScripts } = renderSportPage({ + sportData: parsedCricketMatchData, + nav: parseNav(cricketMatchPageValidated.nav), + }); res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html); }; @@ -264,10 +271,6 @@ const parseFEFootballMatch = ( matchUrl: data.matchUrl, matchHeaderUrl: headerUrl.value, kind: 'FootballMatchSummary', - nav: { - ...extractNAV(data.nav), - selectedPillar: Pillar.Sport, - }, editionId: data.editionId, guardianBaseURL: data.guardianBaseURL, config: data.config, @@ -285,6 +288,22 @@ export const handleFootballMatchPage: RequestHandler = ({ body }, res) => { footballMatchPageValidated, ); - const { html, prefetchScripts } = renderSportPage(parsedFootballMatchData); + const { html, prefetchScripts } = renderSportPage({ + sportData: parsedFootballMatchData, + nav: parseNav(footballMatchPageValidated.nav), + }); + res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html); +}; + +export const handleAppsFootballMatchPage: RequestHandler = ({ body }, res) => { + const footballMatchPageValidated: FEFootballMatchInfoPage = + validateAsFootballMatchPageType(body); + const parsedFootballMatchData = parseFEFootballMatch( + footballMatchPageValidated, + ); + + const { html, prefetchScripts } = renderAppsSportPage( + parsedFootballMatchData, + ); res.status(200).set('Link', makePrefetchHeader(prefetchScripts)).send(html); }; diff --git a/dotcom-rendering/src/server/render.sportDataPage.app.tsx b/dotcom-rendering/src/server/render.sportDataPage.app.tsx new file mode 100644 index 00000000000..259069d9c9b --- /dev/null +++ b/dotcom-rendering/src/server/render.sportDataPage.app.tsx @@ -0,0 +1,77 @@ +import { isString } from '@guardian/libs'; +import { ConfigProvider } from '../components/ConfigContext'; +import { SportDataPageComponent } from '../components/SportDataPageComponent'; +import { + ASSET_ORIGIN, + generateScriptTags, + getPathFromManifest, +} from '../lib/assets'; +import { renderToStringWithEmotion } from '../lib/emotion'; +import { createGuardian } from '../model/guardian'; +import type { FootballMatchInfoPage } from '../sportDataPage'; +import type { Config } from '../types/configContext'; +import { htmlPageTemplate } from './htmlPageTemplate'; +import { decideDescription, decideTitle } from './render.sportDataPage.web'; + +export const renderAppsSportPage = (sportData: FootballMatchInfoPage) => { + const renderingTarget = 'Apps'; + + const config: Config = { + renderingTarget, + darkModeAvailable: true, + assetOrigin: ASSET_ORIGIN, + editionId: sportData.editionId, + }; + + const title = decideTitle(sportData); + const description = decideDescription(sportData.kind); + + const { html, extractedCss } = renderToStringWithEmotion( + + + , + ); + + const clientScripts = [ + getPathFromManifest('client.apps', 'index.js'), + ].filter(isString); + const scriptTags = generateScriptTags([...clientScripts]); + + const guardian = createGuardian({ + editionId: sportData.editionId, + stage: sportData.config.stage, + frontendAssetsFullURL: sportData.config.frontendAssetsFullURL, + revisionNumber: sportData.config.revisionNumber, + sentryPublicApiKey: sportData.config.sentryPublicApiKey, + sentryHost: sportData.config.sentryHost, + dfpAccountId: sportData.config.dfpAccountId, + adUnit: sportData.config.adUnit, + ajaxUrl: sportData.config.ajaxUrl, + googletagUrl: sportData.config.googletagUrl, + switches: sportData.config.switches, + abTests: sportData.config.abTests, + serverSideABTests: sportData.config.serverSideABTests, + isPaidContent: sportData.config.isPaidContent, + contentType: sportData.config.contentType, + googleRecaptchaSiteKey: sportData.config.googleRecaptchaSiteKey, + unknownConfig: sportData.config, + }); + + const renderedPage = htmlPageTemplate({ + scriptTags, + css: extractedCss, + html, + title, + description, + canonicalUrl: sportData.canonicalUrl, + guardian, + renderingTarget, + weAreHiring: !!sportData.config.switches.weAreHiring, + config, + }); + + return { html: renderedPage, prefetchScripts: clientScripts }; +}; diff --git a/dotcom-rendering/src/server/render.sportDataPage.web.tsx b/dotcom-rendering/src/server/render.sportDataPage.web.tsx index 560069049ba..034a9bdbdbc 100644 --- a/dotcom-rendering/src/server/render.sportDataPage.web.tsx +++ b/dotcom-rendering/src/server/render.sportDataPage.web.tsx @@ -10,6 +10,7 @@ import { } from '../lib/assets'; import { renderToStringWithEmotion } from '../lib/emotion'; import { polyfillIO } from '../lib/polyfill.io'; +import type { NavType } from '../model/extract-nav'; import { createGuardian } from '../model/guardian'; import type { CricketMatchPage, @@ -23,7 +24,7 @@ import { htmlPageTemplate } from './htmlPageTemplate'; const fromTheGuardian = 'from the Guardian, the world's leading liberal voice'; -const decideDescription = (kind: SportPageKind) => { +export const decideDescription = (kind: SportPageKind) => { switch (kind) { case 'FootballLiveScores': return `Live football scores ${fromTheGuardian}`; @@ -40,7 +41,7 @@ const decideDescription = (kind: SportPageKind) => { } }; -const decideTitle = (sportPage: SportDataPage) => { +export const decideTitle = (sportPage: SportDataPage) => { switch (sportPage.kind) { case 'FootballLiveScores': case 'FootballResults': @@ -97,7 +98,12 @@ const createCricketTitle = (sportPage: CricketMatchPage) => { return `${sportPage.match.competitionName}, ${sportPage.match.venueName} | Cricket | The Guardian`; }; -export const renderSportPage = (sportData: SportDataPage) => { +type Props = { + sportData: SportDataPage; + nav: NavType; +}; + +export const renderSportPage = ({ sportData, nav }: Props) => { const renderingTarget = 'Web'; const darkModeAvailable = @@ -114,7 +120,11 @@ export const renderSportPage = (sportData: SportDataPage) => { const { html, extractedCss } = renderToStringWithEmotion( - + , ); diff --git a/dotcom-rendering/src/server/server.dev.ts b/dotcom-rendering/src/server/server.dev.ts index 3098887a5e9..07abb409dde 100644 --- a/dotcom-rendering/src/server/server.dev.ts +++ b/dotcom-rendering/src/server/server.dev.ts @@ -18,6 +18,7 @@ import { handleAppsAssets } from './handler.assets.apps'; import { handleEditionsCrossword } from './handler.editionsCrossword'; import { handleFront, handleTagPage } from './handler.front.web'; import { + handleAppsFootballMatchPage, handleCricketMatchPage, handleFootballMatchListPage, handleFootballMatchPage, @@ -117,6 +118,7 @@ renderer.get('/FootballMatchListPage/*url', handleFootballMatchListPage); renderer.get('/FootballTablesPage/*url', handleFootballTablesPage); renderer.get('/CricketMatchPage/*url', handleCricketMatchPage); renderer.get('/FootballMatchSummaryPage/*url', handleFootballMatchPage); +renderer.get('/AppsFootballMatchSummaryPage/*url', handleAppsFootballMatchPage); renderer.get('/HostedContent/*url', handleHostedContent); renderer.get('/AppsHostedContent/*url', handleAppsHostedContent); renderer.get('/AppsComponent/thrasher/:name', handleAppsThrasher); @@ -136,6 +138,7 @@ renderer.post('/FootballMatchListPage', handleFootballMatchListPage); renderer.post('/FootballTablesPage', handleFootballTablesPage); renderer.post('/CricketMatchPage', handleCricketMatchPage); renderer.post('/FootballMatchSummaryPage', handleFootballMatchPage); +renderer.post('/AppsFootballMatchSummaryPage', handleAppsFootballMatchPage); renderer.post('/HostedContent', handleHostedContent); renderer.post('/AppsHostedContent', handleAppsHostedContent); renderer.post('/AppsComponent/thrasher/:name', handleAppsThrasher); diff --git a/dotcom-rendering/src/server/server.prod.ts b/dotcom-rendering/src/server/server.prod.ts index ea41b5645e9..b84564d070b 100644 --- a/dotcom-rendering/src/server/server.prod.ts +++ b/dotcom-rendering/src/server/server.prod.ts @@ -19,6 +19,7 @@ import { handleAppsAssets } from './handler.assets.apps'; import { handleEditionsCrossword } from './handler.editionsCrossword'; import { handleFront, handleTagPage } from './handler.front.web'; import { + handleAppsFootballMatchPage, handleCricketMatchPage, handleFootballMatchListPage, handleFootballMatchPage, @@ -68,6 +69,7 @@ export const prodServer = (): void => { app.post('/EditionsCrossword', handleEditionsCrossword); app.post('/AppsHostedContent', handleAppsHostedContent); app.post('/AppsComponent/thrasher/:name', handleAppsThrasher); + app.use('/AppsFootballMatchSummaryPage', handleAppsFootballMatchPage); app.get('/assets/rendered-items-assets', handleAppsAssets); diff --git a/dotcom-rendering/src/sportDataPage.ts b/dotcom-rendering/src/sportDataPage.ts index 7b506e62947..d2cec62b531 100644 --- a/dotcom-rendering/src/sportDataPage.ts +++ b/dotcom-rendering/src/sportDataPage.ts @@ -11,6 +11,7 @@ import type { FESportPageConfig } from './frontend/feFootballDataPage'; import type { EditionId } from './lib/edition'; import type { NavType } from './model/extract-nav'; import type { FooterType } from './types/footer'; +import type { RenderingTarget } from './types/renderingTarget'; export type Region = { name: string; @@ -22,7 +23,6 @@ export type FootballData = SportPageConfig & { }; export type SportPageConfig = { - nav: NavType; editionId: EditionId; guardianBaseURL: string; config: FESportPageConfig; @@ -77,6 +77,21 @@ export type SportDataPage = export type SportPageKind = SportDataPage['kind']; +interface BaseSportDataPage { + sportData: SportDataPage; + renderingTarget: RenderingTarget; +} + +export interface AppSportDataPage extends BaseSportDataPage { + sportData: FootballMatchInfoPage; + renderingTarget: 'Apps'; +} + +export interface WebSportDataPage extends BaseSportDataPage { + nav: NavType; + renderingTarget: 'Web'; +} + export const cleanTeamName = (teamName: string): string => { return teamName .replace('Ladies', '')