From 364bd317972de8b19d04f5aaa6a5b8474dbdc9db Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 25 Nov 2025 17:08:39 +0000 Subject: [PATCH 01/91] Move `sw.js` rewrite to `next.config` --- ws-nextjs-app/middleware.page.ts | 10 ---------- ws-nextjs-app/next.config.js | 13 +++++++++++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/ws-nextjs-app/middleware.page.ts b/ws-nextjs-app/middleware.page.ts index ad8b0cff85f..245b758b2d7 100644 --- a/ws-nextjs-app/middleware.page.ts +++ b/ws-nextjs-app/middleware.page.ts @@ -13,16 +13,6 @@ export default async function middleware(request: NextRequest) { const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; - const LOCAL_DEV_ONLY = isLocalhost && process.env.NODE_ENV !== 'production'; - - // Service worker is registered at the root (e.g. /pidgin) so will work as is on Test/Live - // but will not work on localhost. This middleware rewrites the request to the sw.js file found in the 'public' folder - if (LOCAL_DEV_ONLY) { - if (request.nextUrl.pathname.endsWith('/sw.js')) { - return NextResponse.rewrite(new URL('/sw.js', request.url)); - } - } - if (PRODUCTION_ONLY) { response = await cspHeaderResponse({ request }); } diff --git a/ws-nextjs-app/next.config.js b/ws-nextjs-app/next.config.js index 97512cdf451..91ec174273b 100644 --- a/ws-nextjs-app/next.config.js +++ b/ws-nextjs-app/next.config.js @@ -9,8 +9,7 @@ const assetPrefix = process.env.SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN + process.env.SIMORGH_PUBLIC_STATIC_ASSETS_PATH; -const isLocal = - process.env.SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN?.includes('localhost'); +const isLocal = process.env.SIMORGH_APP_ENV === 'local'; /** @type {import('next').NextConfig} */ module.exports = { @@ -29,6 +28,16 @@ module.exports = { }, async rewrites() { return [ + // Service worker is registered at the root (e.g. /pidgin) so will work as is on Test/Live + // but will not work on localhost. This rewrites requests to the sw.js file found in the 'public' folder + ...(isLocal + ? [ + { + source: '/:path/sw.js', + destination: '/sw.js', + }, + ] + : []), { source: '/:service/og/:id', destination: '/api/:service/og/:id', From b1ac65ec3b8b48350d1784693c58d552a34ecda8 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 25 Nov 2025 17:18:36 +0000 Subject: [PATCH 02/91] Update next.config.js --- ws-nextjs-app/next.config.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/next.config.js b/ws-nextjs-app/next.config.js index 91ec174273b..0200a8bd0b6 100644 --- a/ws-nextjs-app/next.config.js +++ b/ws-nextjs-app/next.config.js @@ -29,7 +29,8 @@ module.exports = { async rewrites() { return [ // Service worker is registered at the root (e.g. /pidgin) so will work as is on Test/Live - // but will not work on localhost. This rewrites requests to the sw.js file found in the 'public' folder + // but will not work on localhost. This rewrites requests from paths outside of root + // to the sw.js file found in the 'public' folder, which is served from the root. ...(isLocal ? [ { From c8e70e7fe137b1319896bce77e5d48ee9f7fb0ee Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 26 Nov 2025 09:21:25 +0000 Subject: [PATCH 03/91] Logic to set headers from `_document` --- ws-nextjs-app/pages/_document.page.tsx | 18 +++++ .../utilities/cspHeaderResponse/index.ts | 70 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index 6e831935602..f4ab0ade8f0 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -38,6 +38,8 @@ import ReverbTemplate from '#src/server/Document/Renderers/ReverbTemplate'; import { PageTypes } from '#app/models/types/global'; import ComponentTracking from '#src/server/Document/Renderers/ComponentTracking'; import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript'; +import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; +import { cspHeaderResponseForNextDocumentContext } from '#nextjs/utilities/cspHeaderResponse'; import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders'; import derivePageType from '../utilities/derivePageType'; @@ -83,6 +85,20 @@ const handleServerLogging = ({ } }; +const addServiceChainAndCspHeaders = async (ctx: DocumentContext) => { + ctx.res?.setHeader( + 'req-svc-chain', + addPlatformToRequestChainHeader({ + // TODO: Fix type casting + headers: ctx.req?.headers as unknown as Headers, + }), + ); + + if (process.env.NODE_ENV === 'production') { + await cspHeaderResponseForNextDocumentContext({ ctx }); + } +}; + type DocProps = { clientSideEnvVariables: EnvConfig; css: string; @@ -103,6 +119,8 @@ export default class AppDocument extends Document { const { isApp, isAmp, isLite } = getPathExtension(url); + await addServiceChainAndCspHeaders(ctx); + const cache = createCache({ key: 'css' }); const { extractCritical } = createEmotionServer(cache); diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index e1fb4f23336..f477a602047 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -6,6 +6,7 @@ import isLiveEnv from '#lib/utilities/isLive'; import getToggles from '#app/lib/utilities/getToggles/withCache'; import { Services } from '#app/models/types/global'; import SERVICES from '#app/lib/config/services'; +import { DocumentContext } from 'next/document'; const setReportTo = (header: Headers) => { header.set( @@ -59,6 +60,75 @@ const isValidService = (str: string) => { return service && SERVICES.includes(service); }; +export const cspHeaderResponseForNextDocumentContext = async ({ + ctx, +}: { + ctx: DocumentContext; +}) => { + const { isAmp } = getPathExtension(ctx.req?.url || ''); + const isLive = isLiveEnv(); + const urlPath = ctx.req?.url || ''; + let hasAdsScripts = false; + let countryList = ''; + + if (isValidService(urlPath)) { + const service = fallbackServiceParam(ctx.req?.url || ''); + const toggles = await getToggles(service); + + ({ enabled: hasAdsScripts, value: countryList = '' } = + toggles?.adsNonce || { enabled: false, value: '' }); + } + + const country = + ctx?.req?.headers?.['x-country'] || + ctx?.req?.headers?.['x-bbc-edge-country']; + + const shouldServeRelaxedCsp = + hasAdsScripts && + isRelaxedCspEnabled(countryList, (country as string) || ''); + + const { directives } = cspDirectives({ + isAmp, + isLive, + shouldServeRelaxedCsp, + }); + + const BUMP4SpecificConditions = { + 'media-src': ['https:', 'blob:'], + 'connect-src': ['https:'], + }; + + const contentSecurityPolicyHeaderValue = directiveToString({ + ...directives, + ...BUMP4SpecificConditions, + }); + + ctx.res?.setHeader( + 'Content-Security-Policy', + contentSecurityPolicyHeaderValue, + ); + + ctx.res?.setHeader( + 'report-to', + JSON.stringify({ + group: 'worldsvc', + max_age: 2592000, + endpoints: [ + { + url: process.env.SIMORGH_CSP_REPORTING_ENDPOINT, + priority: 1, + }, + ], + include_subdomains: true, + }), + ); + + ctx.res?.setHeader( + 'Content-Security-Policy', + contentSecurityPolicyHeaderValue, + ); +}; + const cspHeaderResponse = async ({ request }: { request: NextRequest }) => { const { isAmp } = getPathExtension(request.url); const isLive = isLiveEnv(); From 86b716f49199e1ce48780f1402a61064ae2e4207 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 26 Nov 2025 09:21:35 +0000 Subject: [PATCH 04/91] Fix `tsc` error --- .../pages/[service]/articles/handleArticleRoute.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts index 45e2fd47c92..a212ccdbde4 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts @@ -43,7 +43,7 @@ describe('handleArticleRoute', () => { it('returns correct cache-control header if article is older than six hours', async () => { jest.spyOn(Date, 'now').mockImplementation(() => 2673964957894); - const result = await handleArticleRoute(mockGetServerSidePropsContext); + await handleArticleRoute(mockGetServerSidePropsContext); expect(mockSetHeader).toHaveBeenCalledWith( 'Cache-Control', @@ -54,7 +54,7 @@ describe('handleArticleRoute', () => { it('returns correct cache-control header if article is not older than six hours', async () => { jest.spyOn(Date, 'now').mockImplementation(() => 1673964987894); - const result = await handleArticleRoute(mockGetServerSidePropsContext); + await handleArticleRoute(mockGetServerSidePropsContext); expect(mockSetHeader).toHaveBeenCalledWith( 'Cache-Control', From 73a7cb7e7a1f343b73db656f43562bd3c249500a Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 26 Nov 2025 09:21:41 +0000 Subject: [PATCH 05/91] Remove `middleware` --- ws-nextjs-app/middleware.page.ts | 32 -------------------------------- 1 file changed, 32 deletions(-) delete mode 100644 ws-nextjs-app/middleware.page.ts diff --git a/ws-nextjs-app/middleware.page.ts b/ws-nextjs-app/middleware.page.ts deleted file mode 100644 index 245b758b2d7..00000000000 --- a/ws-nextjs-app/middleware.page.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NextResponse, NextRequest } from 'next/server'; - -import addPlatformToRequestChainHeader from '#server/utilities/addPlatformToRequestChainHeader'; -import cspHeaderResponse from './utilities/cspHeaderResponse'; - -const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; - -export default async function middleware(request: NextRequest) { - const hostname = request.headers.get('host') ?? request.nextUrl.hostname; - let response = NextResponse.next(); - - const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); - - const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; - - if (PRODUCTION_ONLY) { - response = await cspHeaderResponse({ request }); - } - - response.headers.set( - 'req-svc-chain', - addPlatformToRequestChainHeader({ - headers: request.headers, - }), - ); - - return response; -} - -export const config = { - runtime: 'nodejs', -}; From da25265aa1a9f46e525768676c61005dcf342f03 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 26 Nov 2025 10:07:38 +0000 Subject: [PATCH 06/91] Check hostname --- ws-nextjs-app/pages/_document.page.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index f4ab0ade8f0..ede0cda2329 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -85,6 +85,8 @@ const handleServerLogging = ({ } }; +const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; + const addServiceChainAndCspHeaders = async (ctx: DocumentContext) => { ctx.res?.setHeader( 'req-svc-chain', @@ -94,7 +96,13 @@ const addServiceChainAndCspHeaders = async (ctx: DocumentContext) => { }), ); - if (process.env.NODE_ENV === 'production') { + const hostname = ctx.req?.headers.host || ''; + + const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); + + const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; + + if (PRODUCTION_ONLY) { await cspHeaderResponseForNextDocumentContext({ ctx }); } }; From efffba44b444088f0d9cb2520ae3ed69eb34451d Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 26 Nov 2025 10:10:53 +0000 Subject: [PATCH 07/91] Update _document.page.tsx --- ws-nextjs-app/pages/_document.page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index ede0cda2329..df945027d73 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -91,7 +91,6 @@ const addServiceChainAndCspHeaders = async (ctx: DocumentContext) => { ctx.res?.setHeader( 'req-svc-chain', addPlatformToRequestChainHeader({ - // TODO: Fix type casting headers: ctx.req?.headers as unknown as Headers, }), ); From 1c90626e1518c8ac1c809405d0eadc94305c5a5c Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 26 Nov 2025 10:11:39 +0000 Subject: [PATCH 08/91] Update index.ts --- ws-nextjs-app/utilities/cspHeaderResponse/index.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index f477a602047..e1b0ffab1e2 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -65,14 +65,15 @@ export const cspHeaderResponseForNextDocumentContext = async ({ }: { ctx: DocumentContext; }) => { - const { isAmp } = getPathExtension(ctx.req?.url || ''); + const reqUrl = ctx.req?.url || ''; + const { isAmp } = getPathExtension(reqUrl); const isLive = isLiveEnv(); - const urlPath = ctx.req?.url || ''; + let hasAdsScripts = false; let countryList = ''; - if (isValidService(urlPath)) { - const service = fallbackServiceParam(ctx.req?.url || ''); + if (isValidService(reqUrl)) { + const service = fallbackServiceParam(reqUrl); const toggles = await getToggles(service); ({ enabled: hasAdsScripts, value: countryList = '' } = From 29abfc2f095f004f6056dd7225a805f6a454df5a Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 15:49:14 +0000 Subject: [PATCH 09/91] Replace `cspHeaderResponse` with `DocumentContext` version --- ws-nextjs-app/pages/_document.page.tsx | 4 +- .../utilities/cspHeaderResponse/index.test.ts | 105 ++++++++++-------- .../utilities/cspHeaderResponse/index.ts | 83 +------------- 3 files changed, 60 insertions(+), 132 deletions(-) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index df945027d73..616bca72a33 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -39,7 +39,7 @@ import { PageTypes } from '#app/models/types/global'; import ComponentTracking from '#src/server/Document/Renderers/ComponentTracking'; import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript'; import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; -import { cspHeaderResponseForNextDocumentContext } from '#nextjs/utilities/cspHeaderResponse'; +import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders'; import derivePageType from '../utilities/derivePageType'; @@ -102,7 +102,7 @@ const addServiceChainAndCspHeaders = async (ctx: DocumentContext) => { const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; if (PRODUCTION_ONLY) { - await cspHeaderResponseForNextDocumentContext({ ctx }); + await cspHeaderResponse({ ctx }); } }; diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index e4b58097c00..d99b9c44003 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -1,21 +1,28 @@ -/** - * @jest-environment node - */ -import { NextRequest } from 'next/server'; import getToggles from '#app/lib/utilities/getToggles'; +import { DocumentContext } from 'next/document'; import cspHeaderResponse from '.'; jest.mock('#app/lib/utilities/getToggles'); const mockGetToggles = getToggles as jest.MockedFunction; -const createRequest = (pathname: string, country?: string) => { +const createDocumentContext = ( + pathname: string, + country?: string, +): DocumentContext => { const url = new URL(`https://www.test.bbc.com${pathname}`); const headers = new Headers({ 'x-country': `${country}` }); + return { - nextUrl: url, - headers, - } as NextRequest; + req: { + url: url.pathname, + headers: Object.fromEntries(headers.entries()), + }, + res: { + getHeader: jest.fn(), + setHeader: jest.fn(), + }, + } as unknown as DocumentContext; }; const policies = [ @@ -35,25 +42,15 @@ const policies = [ describe('cspHeaderResponse', () => { it.each(policies)('should set %s in the request CSP', async policy => { - const response = await cspHeaderResponse({ - request: createRequest('/pidgin/live/c7p765ynk9qt'), - }); - - const requestCsp = response.headers.get( - 'x-middleware-request-content-security-policy', - ); + const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - expect(requestCsp?.includes(policy)).toBe(true); - }); - - it.each(policies)('should set %s in the response CSP', async policy => { - const response = await cspHeaderResponse({ - request: createRequest('/pidgin/live/c7p765ynk9qt'), - }); + await cspHeaderResponse({ ctx }); - const requestCsp = response.headers.get('content-security-policy'); + const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( + call => call[0] === 'Content-Security-Policy', + )?.[1]; - expect(requestCsp?.includes(policy)).toBe(true); + expect((requestCsp as string).includes(policy)).toBe(true); }); }); @@ -72,51 +69,63 @@ describe('shouldServeRelaxedCsp', () => { mockGetToggles.mockResolvedValue({ adsNonce: { enabled: true, value: '' }, }); - const response = await cspHeaderResponse({ - request: createRequest('/pidgin/live/c7p765ynk9qt', 'gb'), - }); - expect(response.headers.get('content-security-policy')).toEqual( - expectedRelaxedCsp, - ); + const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); + + await cspHeaderResponse({ ctx }); + + const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( + call => call[0] === 'Content-Security-Policy', + )?.[1]; + + expect(requestCsp).toEqual(expectedRelaxedCsp); }); it('returns true when country is not in omittedCountries', async () => { mockGetToggles.mockResolvedValue({ adsNonce: { enabled: true, value: 'gb' }, }); - const response = await cspHeaderResponse({ - request: createRequest('/pidgin/live/c7p765ynk9qt', 'ax'), - }); - expect(response.headers.get('content-security-policy')).toEqual( - expectedRelaxedCsp, - ); + const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'ax'); + + await cspHeaderResponse({ ctx }); + + const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( + call => call[0] === 'Content-Security-Policy', + )?.[1]; + + expect(requestCsp).toEqual(expectedRelaxedCsp); }); it('returns false when toggle is enabled and given country is in omittedCountries', async () => { mockGetToggles.mockResolvedValue({ adsNonce: { enabled: true, value: 'gb,es' }, }); - const response = await cspHeaderResponse({ - request: createRequest('/pidgin/live/c7p765ynk9qt', 'gb'), - }); - expect(response.headers.get('content-security-policy')).toEqual( - expectedFullCsp, - ); + const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); + + await cspHeaderResponse({ ctx }); + + const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( + call => call[0] === 'Content-Security-Policy', + )?.[1]; + + expect(requestCsp).toEqual(expectedFullCsp); }); it('returns false when adsNonce.enabled is false', async () => { mockGetToggles.mockResolvedValue({ adsNonce: { enabled: false, value: '' }, }); - const response = await cspHeaderResponse({ - request: createRequest('/pidgin/live/c7p765ynk9qt', 'gb'), - }); - expect(response.headers.get('content-security-policy')).toEqual( - expectedFullCsp, - ); + const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); + + await cspHeaderResponse({ ctx }); + + const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( + call => call[0] === 'Content-Security-Policy', + )?.[1]; + + expect(requestCsp).toEqual(expectedFullCsp); }); }); diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index e1b0ffab1e2..f8c0a87c899 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -1,4 +1,3 @@ -import { NextRequest, NextResponse } from 'next/server'; import { cspDirectives } from '#server/utilities/cspHeader/directives'; import fallbackServiceParam from '#app/routes/utils/fetchPageData/utils/getRouteProps/fallbackServiceParam'; import getPathExtension from '#app/utilities/getPathExtension'; @@ -8,23 +7,6 @@ import { Services } from '#app/models/types/global'; import SERVICES from '#app/lib/config/services'; import { DocumentContext } from 'next/document'; -const setReportTo = (header: Headers) => { - header.set( - 'report-to', - JSON.stringify({ - group: 'worldsvc', - max_age: 2592000, - endpoints: [ - { - url: process.env.SIMORGH_CSP_REPORTING_ENDPOINT, - priority: 1, - }, - ], - include_subdomains: true, - }), - ); -}; - const directiveToString = (directives: Record) => { const map = new Map(Object.entries(directives)); let cspValue = ''; @@ -60,11 +42,7 @@ const isValidService = (str: string) => { return service && SERVICES.includes(service); }; -export const cspHeaderResponseForNextDocumentContext = async ({ - ctx, -}: { - ctx: DocumentContext; -}) => { +const cspHeaderResponse = async ({ ctx }: { ctx: DocumentContext }) => { const reqUrl = ctx.req?.url || ''; const { isAmp } = getPathExtension(reqUrl); const isLive = isLiveEnv(); @@ -130,63 +108,4 @@ export const cspHeaderResponseForNextDocumentContext = async ({ ); }; -const cspHeaderResponse = async ({ request }: { request: NextRequest }) => { - const { isAmp } = getPathExtension(request.url); - const isLive = isLiveEnv(); - const urlPath = request.nextUrl.pathname; - let hasAdsScripts = false; - let countryList = ''; - - if (isValidService(urlPath)) { - const service = fallbackServiceParam(request.nextUrl.pathname); - const toggles = await getToggles(service); - - ({ enabled: hasAdsScripts, value: countryList = '' } = - toggles?.adsNonce || { enabled: false, value: '' }); - } - - const requestHeaders = new Headers(request.headers); - const country = - requestHeaders.get('x-country') || requestHeaders.get('x-bbc-edge-country'); - const shouldServeRelaxedCsp = - hasAdsScripts && isRelaxedCspEnabled(countryList, country || ''); - - const { directives } = cspDirectives({ - isAmp, - isLive, - shouldServeRelaxedCsp, - }); - - const BUMP4SpecificConditions = { - 'media-src': ['https:', 'blob:'], - 'connect-src': ['https:'], - }; - - const contentSecurityPolicyHeaderValue = directiveToString({ - ...directives, - ...BUMP4SpecificConditions, - }); - - requestHeaders.set( - 'Content-Security-Policy', - contentSecurityPolicyHeaderValue, - ); - setReportTo(requestHeaders); - - const responseInit = { - request: { - headers: requestHeaders, - }, - }; - - const cspAlteredResponse = NextResponse.next(responseInit); - cspAlteredResponse.headers.set( - 'Content-Security-Policy', - contentSecurityPolicyHeaderValue, - ); - setReportTo(cspAlteredResponse.headers); - - return cspAlteredResponse; -}; - export default cspHeaderResponse; From 157e31212ce7c6d0e0afebaa88d8b739ff7b37dd Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 16:12:21 +0000 Subject: [PATCH 10/91] Fetch toggles from `_document.page.tsx` --- ws-nextjs-app/pages/_document.page.tsx | 16 ++++-- .../utilities/cspHeaderResponse/index.test.ts | 49 +++++++++---------- .../utilities/cspHeaderResponse/index.ts | 16 +++--- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index 616bca72a33..9622c4fd765 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -35,11 +35,12 @@ import NO_JS_CLASSNAME from '#app/lib/noJs.const'; import getPathExtension from '#app/utilities/getPathExtension'; import ReverbTemplate from '#src/server/Document/Renderers/ReverbTemplate'; -import { PageTypes } from '#app/models/types/global'; +import { PageTypes, Toggles } from '#app/models/types/global'; import ComponentTracking from '#src/server/Document/Renderers/ComponentTracking'; import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript'; import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; +import getToggles from '#app/lib/utilities/getToggles/withCache'; import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders'; import derivePageType from '../utilities/derivePageType'; @@ -87,7 +88,10 @@ const handleServerLogging = ({ const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; -const addServiceChainAndCspHeaders = async (ctx: DocumentContext) => { +const addServiceChainAndCspHeaders = async ( + ctx: DocumentContext, + toggles: Toggles, +) => { ctx.res?.setHeader( 'req-svc-chain', addPlatformToRequestChainHeader({ @@ -102,7 +106,7 @@ const addServiceChainAndCspHeaders = async (ctx: DocumentContext) => { const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; if (PRODUCTION_ONLY) { - await cspHeaderResponse({ ctx }); + await cspHeaderResponse({ ctx, toggles }); } }; @@ -126,7 +130,9 @@ export default class AppDocument extends Document { const { isApp, isAmp, isLite } = getPathExtension(url); - await addServiceChainAndCspHeaders(ctx); + const toggles = await getToggles(); + + await addServiceChainAndCspHeaders(ctx, toggles); const cache = createCache({ key: 'css' }); const { extractCritical } = createEmotionServer(cache); @@ -136,7 +142,7 @@ export default class AppDocument extends Document { originalRenderPage({ enhanceApp: App => props => ( - + ), }); diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index d99b9c44003..1095422081d 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -1,11 +1,6 @@ -import getToggles from '#app/lib/utilities/getToggles'; import { DocumentContext } from 'next/document'; import cspHeaderResponse from '.'; -jest.mock('#app/lib/utilities/getToggles'); - -const mockGetToggles = getToggles as jest.MockedFunction; - const createDocumentContext = ( pathname: string, country?: string, @@ -44,7 +39,7 @@ describe('cspHeaderResponse', () => { it.each(policies)('should set %s in the request CSP', async policy => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - await cspHeaderResponse({ ctx }); + await cspHeaderResponse({ ctx, toggles: {} }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', @@ -66,13 +61,14 @@ describe('shouldServeRelaxedCsp', () => { "default-src 'self' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.googlesyndication.com;child-src 'self';connect-src https:;font-src *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com data: https://*.teads.tv https://cdnjs.cloudflare.com/ajax/libs/font-awesome/ https://fonts.gstatic.com;frame-src 'self' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.amazon-adsystem.com https://*.chartbeat.com https://*.doubleclick.net https://*.facebook.com https://*.google.com https://*.googleadservices.com https://*.googlesyndication.com https://*.mapcreator.io https://*.teads.tv https://*.thomsonreuters.com https://*.twitter.com https://bbc-maps.carto.com https://bbc.com https://cdn.privacy-mgmt.com https://chartbeat.com https://edigitalsurvey.com https://flo.uri.sh https://public.flourish.studio https://www.instagram.com https://www.riddle.com https://www.tiktok.com https://www.youtube-nocookie.com https://www.youtube.com;img-src *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com data: 'self' http://ping.chartbeat.net https://*.adsafeprotected.com https://*.amazon-adsystem.com https://*.cdninstagram.com https://*.doubleclick.net https://*.effectivemeasure.net https://*.google.com https://*.googlesyndication.com https://*.googleusercontent.com https://*.gstatic.com https://*.imrworldwide.com https://*.teads.tv https://*.tiktokcdn.com https://*.twimg.com https://*.twitter.com https://*.xx.fbcdn.net https://i.ytimg.com https://logws1363.ati-host.net https://ping.chartbeat.net https://sb.scorecardresearch.com https://www.googleadservices.com;script-src 'self' 'unsafe-eval' 'unsafe-inline' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com http://*.chartbeat.com http://localhost:1124 http://localhost:7080 https://*.adsafeprotected.com https://*.amazon-adsystem.com https://*.chartbeat.com https://*.covatic.io https://*.doubleverify.com https://*.effectivemeasure.net https://*.facebook.com https://*.g.doubleclick.net https://*.google.ae https://*.google.at https://*.google.az https://*.google.be https://*.google.ca https://*.google.ch https://*.google.cl https://*.google.co.id https://*.google.co.il https://*.google.co.in https://*.google.co.jp https://*.google.co.kr https://*.google.co.nz https://*.google.co.tz https://*.google.co.ve https://*.google.com https://*.google.com.af https://*.google.com.ar https://*.google.com.au https://*.google.com.bo https://*.google.com.br https://*.google.com.co https://*.google.com.cy https://*.google.com.ec https://*.google.com.eg https://*.google.com.gt https://*.google.com.hk https://*.google.com.kh https://*.google.com.mm https://*.google.com.mt https://*.google.com.mx https://*.google.com.ng https://*.google.com.ni https://*.google.com.np https://*.google.com.pe https://*.google.com.pk https://*.google.com.pr https://*.google.com.py https://*.google.com.ro https://*.google.com.sa https://*.google.com.sg https://*.google.com.sv https://*.google.com.tr https://*.google.com.tw https://*.google.com.ua https://*.google.com.uy https://*.google.com.vn https://*.google.cv https://*.google.cz https://*.google.de https://*.google.dk https://*.google.es https://*.google.fi https://*.google.fr https://*.google.ge https://*.google.hn https://*.google.ie https://*.google.iq https://*.google.it https://*.google.jo https://*.google.kz https://*.google.lk https://*.google.lv https://*.google.nl https://*.google.no https://*.google.pl https://*.google.ru https://*.google.se https://*.google.so https://*.googlesyndication.com https://*.imrworldwide.com https://*.mapcreator.io https://*.permutive.com https://*.teads.tv https://*.thomsonreuters.com https://*.twimg.com https://*.twitter.com https://*.webcontentassessor.com https://*.xx.fbcdn.net https://adservice.google.co.uk https://bbc.gscontxt.net https://cdn.ampproject.org https://cdn.privacy-mgmt.com https://connect.facebook.net https://lf16-tiktok-web.ttwstatic.com https://public.flourish.studio https://sb.scorecardresearch.com https://www.googletagservices.com https://www.instagram.com https://www.riddle.com https://www.tiktok.com;style-src 'unsafe-inline' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.twimg.com https://*.twitter.com https://*.xx.fbcdn.net https://fonts.googleapis.com https://lf16-tiktok-web.ttwstatic.com;media-src https: blob:;worker-src blob: 'self' *.bbc.co.uk *.bbc.com;report-to worldsvc;upgrade-insecure-requests;"; it('returns true when toggle is enabled but does not have values set', async () => { - mockGetToggles.mockResolvedValue({ - adsNonce: { enabled: true, value: '' }, - }); - const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - await cspHeaderResponse({ ctx }); + await cspHeaderResponse({ + ctx, + toggles: { + adsNonce: { enabled: true, value: '' }, + }, + }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', @@ -82,13 +78,14 @@ describe('shouldServeRelaxedCsp', () => { }); it('returns true when country is not in omittedCountries', async () => { - mockGetToggles.mockResolvedValue({ - adsNonce: { enabled: true, value: 'gb' }, - }); - const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'ax'); - await cspHeaderResponse({ ctx }); + await cspHeaderResponse({ + ctx, + toggles: { + adsNonce: { enabled: true, value: 'gb' }, + }, + }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', @@ -98,13 +95,14 @@ describe('shouldServeRelaxedCsp', () => { }); it('returns false when toggle is enabled and given country is in omittedCountries', async () => { - mockGetToggles.mockResolvedValue({ - adsNonce: { enabled: true, value: 'gb,es' }, - }); - const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); - await cspHeaderResponse({ ctx }); + await cspHeaderResponse({ + ctx, + toggles: { + adsNonce: { enabled: true, value: 'gb,es' }, + }, + }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', @@ -114,13 +112,12 @@ describe('shouldServeRelaxedCsp', () => { }); it('returns false when adsNonce.enabled is false', async () => { - mockGetToggles.mockResolvedValue({ - adsNonce: { enabled: false, value: '' }, - }); - const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); - await cspHeaderResponse({ ctx }); + await cspHeaderResponse({ + ctx, + toggles: { adsNonce: { enabled: false, value: '' } }, + }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index f8c0a87c899..f54e2d33446 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -1,9 +1,7 @@ import { cspDirectives } from '#server/utilities/cspHeader/directives'; -import fallbackServiceParam from '#app/routes/utils/fetchPageData/utils/getRouteProps/fallbackServiceParam'; import getPathExtension from '#app/utilities/getPathExtension'; import isLiveEnv from '#lib/utilities/isLive'; -import getToggles from '#app/lib/utilities/getToggles/withCache'; -import { Services } from '#app/models/types/global'; +import { Services, Toggles } from '#app/models/types/global'; import SERVICES from '#app/lib/config/services'; import { DocumentContext } from 'next/document'; @@ -42,7 +40,13 @@ const isValidService = (str: string) => { return service && SERVICES.includes(service); }; -const cspHeaderResponse = async ({ ctx }: { ctx: DocumentContext }) => { +const cspHeaderResponse = async ({ + ctx, + toggles, +}: { + ctx: DocumentContext; + toggles: Toggles; +}) => { const reqUrl = ctx.req?.url || ''; const { isAmp } = getPathExtension(reqUrl); const isLive = isLiveEnv(); @@ -51,10 +55,8 @@ const cspHeaderResponse = async ({ ctx }: { ctx: DocumentContext }) => { let countryList = ''; if (isValidService(reqUrl)) { - const service = fallbackServiceParam(reqUrl); - const toggles = await getToggles(service); - ({ enabled: hasAdsScripts, value: countryList = '' } = + // @ts-expect-error- Toggles type issue toggles?.adsNonce || { enabled: false, value: '' }); } From 96d15ab4d2df2085340db99541468fdfea8ca9d9 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 16:13:17 +0000 Subject: [PATCH 11/91] Pass args as object --- ws-nextjs-app/pages/_document.page.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index 9622c4fd765..83c2d1e1735 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -88,10 +88,13 @@ const handleServerLogging = ({ const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; -const addServiceChainAndCspHeaders = async ( - ctx: DocumentContext, - toggles: Toggles, -) => { +const addServiceChainAndCspHeaders = async ({ + ctx, + toggles, +}: { + ctx: DocumentContext; + toggles: Toggles; +}) => { ctx.res?.setHeader( 'req-svc-chain', addPlatformToRequestChainHeader({ @@ -132,7 +135,7 @@ export default class AppDocument extends Document { const toggles = await getToggles(); - await addServiceChainAndCspHeaders(ctx, toggles); + await addServiceChainAndCspHeaders({ ctx, toggles }); const cache = createCache({ key: 'css' }); const { extractCritical } = createEmotionServer(cache); From 5d2c6373a1f9d34364e8367b4f656f992c2ef5e6 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 16:25:36 +0000 Subject: [PATCH 12/91] Add `extractHeaders` function to global page props --- ws-nextjs-app/pages/_document.page.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index 83c2d1e1735..44792387230 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -41,6 +41,7 @@ import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript' import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; import getToggles from '#app/lib/utilities/getToggles/withCache'; +import extractHeaders from '#server/utilities/extractHeaders'; import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders'; import derivePageType from '../utilities/derivePageType'; @@ -145,7 +146,18 @@ export default class AppDocument extends Document { originalRenderPage({ enhanceApp: App => props => ( - + ), }); From 3bfde8d18b4e6438b390675ae3065f63efee1b92 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 16:51:04 +0000 Subject: [PATCH 13/91] Pass `isNextJs: true` down --- ws-nextjs-app/pages/_document.page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index 44792387230..d674c9b0bf3 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -156,6 +156,7 @@ export default class AppDocument extends Document { isApp, isAmp, isLite, + isNextJs: true, }} /> From 6925ed3079289c28d2719fcca3e7478f99f93dde Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 16:51:48 +0000 Subject: [PATCH 14/91] Remove toggle check from `augmentWithDisclaimer` The toggle is checked within the `Disclaimer` component itself, so this seems redundant to just not add the small block type to the page data --- .../utils/augmentWithDisclaimer.test.ts | 18 ------------------ .../article/utils/augmentWithDisclaimer.ts | 13 ++----------- 2 files changed, 2 insertions(+), 29 deletions(-) diff --git a/src/app/routes/article/utils/augmentWithDisclaimer.test.ts b/src/app/routes/article/utils/augmentWithDisclaimer.test.ts index fb4fa8f02fd..34459421584 100644 --- a/src/app/routes/article/utils/augmentWithDisclaimer.test.ts +++ b/src/app/routes/article/utils/augmentWithDisclaimer.test.ts @@ -10,16 +10,9 @@ const buildPageDataFixture = (blocks = [{ type: 'timestamp' }]) => }, }) as Article; -const buildTogglesFixture = (enabled = true) => ({ - disclaimer: { - enabled, - }, -}); - describe('augmentWithDisclaimer', () => { it('Should put the disclaimer after the timestamp if positionFromTimestamp is 1', () => { const transformedData = transformer({ - toggles: buildTogglesFixture(), positionFromTimestamp: 1, })(buildPageDataFixture()) as Article; @@ -29,7 +22,6 @@ describe('augmentWithDisclaimer', () => { it('Should put the disclaimer before the timestamp if positionFromTimestamp is 0', () => { const transformedData = transformer({ - toggles: buildTogglesFixture(), positionFromTimestamp: 0, })(buildPageDataFixture()) as Article; @@ -39,19 +31,9 @@ describe('augmentWithDisclaimer', () => { it('Should put the disclaimer as the first block if the page data has no timestamp', () => { const transformedData = transformer({ - toggles: buildTogglesFixture(), positionFromTimestamp: 0, })(buildPageDataFixture([])) as Article; expect(transformedData.content.model.blocks[0].type).toEqual('disclaimer'); }); - - it('Should not add a disclaimer when toggled off for that service', () => { - const transformedData = transformer({ - toggles: buildTogglesFixture(false), - positionFromTimestamp: 0, - })(buildPageDataFixture([])) as Article; - - expect(transformedData.content.model.blocks[0]).toBeUndefined(); - }); }); diff --git a/src/app/routes/article/utils/augmentWithDisclaimer.ts b/src/app/routes/article/utils/augmentWithDisclaimer.ts index 984aac711c0..1c22c1c6d6d 100644 --- a/src/app/routes/article/utils/augmentWithDisclaimer.ts +++ b/src/app/routes/article/utils/augmentWithDisclaimer.ts @@ -1,14 +1,11 @@ import pathOr from 'ramda/src/pathOr'; -import pathEq from 'ramda/src/pathEq'; import assocPath from 'ramda/src/assocPath'; import insert from 'ramda/src/insert'; import { Article, OptimoBlock } from '#app/models/types/optimo'; -import { Toggles } from '#app/models/types/global'; import { isSfv } from './paramChecks'; const getBlocks = pathOr([], ['content', 'model', 'blocks']); const setBlocks = assocPath(['content', 'model', 'blocks']); -const isDisclaimerToggledOn = pathEq(true, ['disclaimer', 'enabled']); const disclaimerBlock = { type: 'disclaimer', model: {}, @@ -35,14 +32,8 @@ const insertDisclaimer = ( pageData, ); -export default ({ - toggles, - positionFromTimestamp, - }: { - toggles?: Toggles; - positionFromTimestamp: number; - }) => +export default ({ positionFromTimestamp }: { positionFromTimestamp: number }) => (pageData: Article): Article => - isDisclaimerToggledOn(toggles) && !isSfv(pageData) + !isSfv(pageData) ? insertDisclaimer(pageData, positionFromTimestamp) : pageData; From c112fa8e891e6dd05b86732f7678f143838c41aa Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 16:52:08 +0000 Subject: [PATCH 15/91] Remove `toggles` from `getServerSideProps` in pages --- .../[service]/articles/handleArticleRoute.test.ts | 4 ---- .../pages/[service]/articles/handleArticleRoute.ts | 12 +++++------- .../[service]/live/[id]/[[...variant]].page.tsx | 3 +-- .../[service]/send/[id]/[[...variant]].page.tsx | 5 ++--- .../watch/[id]/live/[[...variant]].page.tsx | 4 +--- ws-nextjs-app/pages/ws/languages.page.tsx | 5 +---- 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts index 60dbc3e3d18..11ca56062c6 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts @@ -1,7 +1,6 @@ import pidginMediaArticleFixtureData from '#data/pidgin/articles/cvpde7nqj92o.json'; import { GetServerSidePropsContext } from 'next'; import * as fetchPageData from '#app/routes/utils/fetchPageData'; -import defaultToggles from '#app/lib/config/toggles'; import * as shouldRender from './shouldRender'; import handleArticleRoute from './handleArticleRoute'; @@ -25,7 +24,6 @@ describe('handleArticleRoute', () => { json: pidginMediaArticleFixtureData, }); }); - const toggles = defaultToggles.local; it('returns correct page type if consumableAsSFV is true', async () => { const result = await handleArticleRoute(mockGetServerSidePropsContext); @@ -87,7 +85,6 @@ describe('handleArticleRoute', () => { showAdsBasedOnLocation: false, showCookieBannerBasedOnCountry: true, timeOnServer: 1234567890000, - toggles, variant: null, }, }); @@ -118,7 +115,6 @@ describe('handleArticleRoute', () => { showAdsBasedOnLocation: false, showCookieBannerBasedOnCountry: true, timeOnServer: 1234567890000, - toggles, variant: null, }, }); diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts index 14e8710cd81..898195ecd93 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts @@ -8,7 +8,7 @@ import { ROUTING_INFORMATION } from '#app/lib/logger.const'; import getPathExtension from '#app/utilities/getPathExtension'; import PageDataParams from '#app/models/types/pageDataParams'; import handleError from '#app/routes/utils/handleError'; -import { PageTypes, Toggles } from '#app/models/types/global'; +import { PageTypes } from '#app/models/types/global'; import augmentWithDisclaimer from '#app/routes/article/utils/augmentWithDisclaimer'; import { ArticleMetadata } from '#app/models/types/optimo'; import { getServerExperiments } from '#server/utilities/experimentHeader'; @@ -17,8 +17,8 @@ import getPageData from '../../../utilities/pageRequests/getPageData'; const logger = nodeLogger(__filename); -const transformPageData = (toggles?: Toggles) => - augmentWithDisclaimer({ toggles, positionFromTimestamp: 0 }); +const transformPageData = () => + augmentWithDisclaimer({ positionFromTimestamp: 0 }); const getDerivedArticleType = (metadata: ArticleMetadata) => { let pageType: PageTypes = metadata?.type; @@ -44,7 +44,7 @@ export default async (context: GetServerSidePropsContext) => { const { isAmp, isApp, isLite } = getPathExtension(resolvedUrlWithoutQuery); const { variant } = parseRoute(resolvedUrl); - const { data, toggles } = await getPageData({ + const { data } = await getPageData({ id: resolvedUrlWithoutQuery, service, variant: variant || undefined, @@ -81,7 +81,6 @@ export default async (context: GetServerSidePropsContext) => { variant: variant || null, pageType: ARTICLE_PAGE, pathname: resolvedUrlWithoutQuery, - toggles, ...extractHeaders(reqHeaders), }, }; @@ -111,7 +110,7 @@ export default async (context: GetServerSidePropsContext) => { mediaCuration = null, } = secondaryData; - const transformedArticleData = transformPageData(toggles)(article); + const transformedArticleData = transformPageData()(article); routingInfoLogger(ROUTING_INFORMATION, { url: resolvedUrlWithoutQuery, @@ -151,7 +150,6 @@ export default async (context: GetServerSidePropsContext) => { serverSideExperiments, service, status, - toggles, variant: variant || null, ...extractHeaders(reqHeaders), }, diff --git a/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx index 78ae230b44f..ff5605df305 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx @@ -73,7 +73,7 @@ export const getServerSideProps: GetServerSideProps = async context => { }; } - const { data, toggles } = await getPageData({ + const { data } = await getPageData({ id, page, service, @@ -125,7 +125,6 @@ export const getServerSideProps: GetServerSideProps = async context => { service, status: data.status, timeOnServer: Date.now(), // TODO: check if needed? - toggles, variant, ...extractHeaders(reqHeaders), }, diff --git a/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx index c913bf10cb4..e578250ccf1 100644 --- a/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx @@ -4,7 +4,7 @@ import PageDataParams from '#models/types/pageDataParams'; import { UGC_PAGE } from '#app/routes/utils/pageTypes'; import getPathExtension from '#app/utilities/getPathExtension'; import deriveVariant from '#nextjs/utilities/deriveVariant'; -import extractHeaders from '../../../../../src/server/utilities/extractHeaders'; +import extractHeaders from '#server/utilities/extractHeaders'; import getPageData from '../../../../utilities/pageRequests/getPageData'; const UGCPageLayout = dynamic(() => import('./UGCPageLayout')); @@ -27,7 +27,7 @@ export const getServerSideProps: GetServerSideProps = async context => { const variant = deriveVariant(variantFromUrl); - const { data, toggles } = await getPageData({ + const { data } = await getPageData({ id, service, variant, @@ -59,7 +59,6 @@ export const getServerSideProps: GetServerSideProps = async context => { pathname: context.resolvedUrl, service, status: status ?? 500, - toggles, variant, timeOnServer: Date.now(), // TODO: check if needed? ...extractHeaders(reqHeaders), diff --git a/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx index 4ddd5a36434..9e61886b256 100644 --- a/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx @@ -40,7 +40,7 @@ export const getServerSideProps: GetServerSideProps = async context => { const { headers: reqHeaders } = context.req; - const { data, toggles } = await getPageData({ + const { data } = await getPageData({ id, service, rendererEnv, @@ -68,7 +68,6 @@ export const getServerSideProps: GetServerSideProps = async context => { pageType: LIVE_TV_PAGE as PageTypes, id, service, - toggles, pageData: data?.pageData ? { ...data.pageData, @@ -94,4 +93,3 @@ export const getServerSideProps: GetServerSideProps = async context => { }; export default LiveTvLayout; - diff --git a/ws-nextjs-app/pages/ws/languages.page.tsx b/ws-nextjs-app/pages/ws/languages.page.tsx index 89919b10162..8fcef8342ba 100644 --- a/ws-nextjs-app/pages/ws/languages.page.tsx +++ b/ws-nextjs-app/pages/ws/languages.page.tsx @@ -33,7 +33,7 @@ export const getServerSideProps: GetServerSideProps = async context => { }, }; - const { data, toggles } = await getPageData({ + const { data } = await getPageData({ service: 'ws', rendererEnv, resolvedUrl: '/ws/languages', @@ -48,7 +48,6 @@ export const getServerSideProps: GetServerSideProps = async context => { status: data?.status, pageType: HOME_PAGE, service: 'ws', - toggles, pageData: { metadata: { type: HOME_PAGE, @@ -66,7 +65,6 @@ export const getServerSideProps: GetServerSideProps = async context => { service: 'ws', pathname: '/ws/languages', status: data?.status, - toggles, pageData: { ...data?.pageData, metadata: { @@ -83,4 +81,3 @@ export const getServerSideProps: GetServerSideProps = async context => { }; export default HomePage; - From e845825e41cd934339df6e71a6195b1629255da8 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 16:52:21 +0000 Subject: [PATCH 16/91] Remove `toggles` fetch from `getPageData` --- .../pageRequests/getPageData.test.ts | 25 ++----------------- .../utilities/pageRequests/getPageData.ts | 5 +--- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/ws-nextjs-app/utilities/pageRequests/getPageData.test.ts b/ws-nextjs-app/utilities/pageRequests/getPageData.test.ts index eaf42394a72..6e83d22ec08 100644 --- a/ws-nextjs-app/utilities/pageRequests/getPageData.test.ts +++ b/ws-nextjs-app/utilities/pageRequests/getPageData.test.ts @@ -1,5 +1,4 @@ import * as fetchPageData from '#app/routes/utils/fetchPageData'; -import * as getToggles from '#app/lib/utilities/getToggles'; import * as fetchDataFromBFF from '#app/routes/utils/fetchDataFromBFF'; import getPageData from './getPageData'; @@ -17,18 +16,12 @@ describe('getPageData', () => { it('Returns page data and status 200 for a valid page', async () => { const fetchDataResponse = { title: 'UGC Form Title!' }; - const toggleResponse = { - toggles: { testToggle: { enabled: true } }, - }; - jest.spyOn(fetchPageData, 'default').mockResolvedValue({ status: 200, json: { data: fetchDataResponse }, }); - jest.spyOn(getToggles, 'default').mockResolvedValue(toggleResponse); - - const { data: actualData, toggles: actualToggles } = await getPageData({ + const { data: actualData } = await getPageData({ id: 'u50853489', service: 'mundo', variant: undefined, @@ -41,23 +34,16 @@ describe('getPageData', () => { pageData: fetchDataResponse, status: 200, }); - expect(actualToggles).toStrictEqual(toggleResponse); }); it('Cleans malicious query parameters', async () => { const fetchDataResponse = { title: 'UGC Form Title!' }; - const toggleResponse = { - toggles: { testToggle: { enabled: true } }, - }; - jest.spyOn(fetchPageData, 'default').mockResolvedValue({ status: 200, json: { data: fetchDataResponse }, }); - jest.spyOn(getToggles, 'default').mockResolvedValue(toggleResponse); - const fetchDataFromBFFSpy = jest.spyOn(fetchDataFromBFF, 'default'); await getPageData({ @@ -76,18 +62,13 @@ describe('getPageData', () => { it('Returns page data and status 404 for an invalid page', async () => { const errorMessage = 'Something went wrong!'; - const toggleResponse = { - toggles: { testToggle: { enabled: true } }, - }; jest.spyOn(fetchPageData, 'default').mockRejectedValue({ message: errorMessage, status: 404, }); - jest.spyOn(getToggles, 'default').mockResolvedValue(toggleResponse); - - const { data: actualData, toggles: actualToggles } = await getPageData({ + const { data: actualData } = await getPageData({ id: 'u50853489', service: 'mundo', variant: undefined, @@ -100,8 +81,6 @@ describe('getPageData', () => { error: errorMessage, status: 404, }); - - expect(actualToggles).toStrictEqual(toggleResponse); }); }); }); diff --git a/ws-nextjs-app/utilities/pageRequests/getPageData.ts b/ws-nextjs-app/utilities/pageRequests/getPageData.ts index 6bf86314fac..7959b4c69c6 100644 --- a/ws-nextjs-app/utilities/pageRequests/getPageData.ts +++ b/ws-nextjs-app/utilities/pageRequests/getPageData.ts @@ -1,5 +1,4 @@ import { BFF_FETCH_ERROR } from '#app/lib/logger.const'; -import getToggles from '#app/lib/utilities/getToggles/withCache'; import { FetchError } from '#app/models/types/fetch'; import sendCustomMetric from '#server/utilities/customMetrics'; import { NON_200_RESPONSE } from '#server/utilities/customMetrics/metrics.const'; @@ -72,9 +71,7 @@ const getPageData = async ({ ? { pageData: json.data, status } : { error: message, status }; - const toggles = await getToggles(service); - - return { data, toggles }; + return { data }; }; export default getPageData; From 659d87096629f73757a749b6e64ac0e368625207 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 17:03:22 +0000 Subject: [PATCH 17/91] Remove `toggles` from old article fetcher to fix TS error --- src/app/routes/article/getInitialData/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/routes/article/getInitialData/index.ts b/src/app/routes/article/getInitialData/index.ts index f12ae1ba5e8..75d4e5555e1 100644 --- a/src/app/routes/article/getInitialData/index.ts +++ b/src/app/routes/article/getInitialData/index.ts @@ -18,15 +18,14 @@ type Props = { getAgent: GetAgent; }; -const transformPageData = (toggles?: Toggles) => - augmentWithDisclaimer({ toggles, positionFromTimestamp: 0 }); +const transformPageData = () => + augmentWithDisclaimer({ positionFromTimestamp: 0 }); export default async ({ service, pageType, path: pathname, variant, - toggles, isAmp, getAgent, }: Props) => { @@ -57,7 +56,7 @@ export default async ({ billboardCuration, } = secondaryData; - const transformedArticleData = transformPageData(toggles)(article); + const transformedArticleData = transformPageData()(article); const response = { status, From 17b3974fdf98301787cdeeb2022e8ba7af574f4b Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:00:50 +0000 Subject: [PATCH 18/91] fallback to `index` if no `id` on block --- src/app/legacy/containers/Blocks/index.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/legacy/containers/Blocks/index.jsx b/src/app/legacy/containers/Blocks/index.jsx index 9fa360b9310..011a58e8709 100644 --- a/src/app/legacy/containers/Blocks/index.jsx +++ b/src/app/legacy/containers/Blocks/index.jsx @@ -25,8 +25,9 @@ const Blocks = ({ blocks, componentsToRender }) => : Fragment; const { type: typeOfPreviousBlock } = blocks[index - 1] || {}; + return ( - + Date: Tue, 2 Dec 2025 18:23:25 +0000 Subject: [PATCH 19/91] Remove platform flags from `handleArticleRoute` --- .../[service]/articles/handleArticleRoute.test.ts | 8 -------- .../pages/[service]/articles/handleArticleRoute.ts | 10 +--------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts index 11ca56062c6..361318ff908 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts @@ -73,10 +73,6 @@ describe('handleArticleRoute', () => { expect(result).toEqual({ props: { bbcOrigin: null, - isAmp: false, - isApp: false, - isLite: false, - isNextJs: true, status: 500, isUK: false, pageType: 'article', @@ -103,10 +99,6 @@ describe('handleArticleRoute', () => { expect(result).toEqual({ props: { bbcOrigin: null, - isAmp: false, - isApp: false, - isLite: false, - isNextJs: true, status: 404, isUK: false, pageType: 'article', diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts index 898195ecd93..6fb8a469c6e 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts @@ -41,7 +41,7 @@ export default async (context: GetServerSidePropsContext) => { const resolvedUrlWithoutQuery = resolvedUrl.split('?')?.[0]; - const { isAmp, isApp, isLite } = getPathExtension(resolvedUrlWithoutQuery); + const { isAmp } = getPathExtension(resolvedUrlWithoutQuery); const { variant } = parseRoute(resolvedUrl); const { data } = await getPageData({ @@ -71,10 +71,6 @@ export default async (context: GetServerSidePropsContext) => { return { props: { - isApp, - isAmp, - isLite, - isNextJs: true, service, status: renderStatus, timeOnServer: Date.now(), @@ -130,10 +126,6 @@ export default async (context: GetServerSidePropsContext) => { props: { country: reqHeaders?.['x-country'] || null, id: resolvedUrlWithoutQuery, - isAmp, - isApp, - isLite, - isNextJs: true, pageData: { ...transformedArticleData, secondaryColumn: { From 70f9ffa44a69310d1ab7844943e5d8e809d57bcf Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:23:50 +0000 Subject: [PATCH 20/91] Remove platform flag from `handleAvRoute` --- ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts b/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts index bc12018fe2e..b0c3184bfbe 100644 --- a/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts +++ b/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts @@ -128,7 +128,6 @@ export default async (context: GetServerSidePropsContext) => { return { props: { id: resolvedUrl, - isNextJs: true, isAvEmbeds: true, pageData: avEmbed ? { From 2302978300b2a3b9eb5116779e81d9b46542cc6f Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:24:09 +0000 Subject: [PATCH 21/91] Remove platform flags from `downloads` page --- ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx index 8c2182a410f..5182f7d1071 100644 --- a/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx @@ -50,8 +50,6 @@ export const getServerSideProps: GetServerSideProps = async context => { return { props: { error: null, - isAmp: false, - isNextJs: true, pageData: { downloadData, metadata: { From 8987c09a4cbd3598cb3c2b2d4e8a15bec2fc9612 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:24:43 +0000 Subject: [PATCH 22/91] Remove platform flags from `live` page --- .../pages/[service]/live/[id]/[[...variant]].page.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx index ff5605df305..08f72ba788f 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx @@ -4,8 +4,6 @@ import isLive from '#app/lib/utilities/isLive'; import { LIVE_PAGE } from '#app/routes/utils/pageTypes'; import nodeLogger from '#lib/logger.node'; import logResponseTime from '#server/utilities/logResponseTime'; - -import getPathExtension from '#app/utilities/getPathExtension'; import { ROUTING_INFORMATION } from '#app/lib/logger.const'; import { OK } from '#app/lib/statusCodes.const'; import sendCustomMetric from '#server/utilities/customMetrics'; @@ -45,8 +43,6 @@ export const getServerSideProps: GetServerSideProps = async context => { const { headers: reqHeaders } = context.req; - const { isApp, isLite } = getPathExtension(context.resolvedUrl); - const variant = deriveVariant(variantFromUrl); if (!isValidPageNumber(page)) { @@ -61,9 +57,6 @@ export const getServerSideProps: GetServerSideProps = async context => { return { props: { - isApp, - isLite, - isNextJs: true, service, status: 404, timeOnServer: Date.now(), @@ -105,10 +98,6 @@ export const getServerSideProps: GetServerSideProps = async context => { props: { error: data?.error || null, id, - isApp, - isLite, - isAmp: false, - isNextJs: true, page: page || null, pageData: data?.pageData ? { From fe530459df92e0c262eea64122594ba5c681af2c Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:25:03 +0000 Subject: [PATCH 23/91] Remove platform flags from `send` page --- .../pages/[service]/send/[id]/[[...variant]].page.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx index e578250ccf1..f435454f87f 100644 --- a/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx @@ -2,7 +2,6 @@ import { GetServerSideProps } from 'next'; import dynamic from 'next/dynamic'; import PageDataParams from '#models/types/pageDataParams'; import { UGC_PAGE } from '#app/routes/utils/pageTypes'; -import getPathExtension from '#app/utilities/getPathExtension'; import deriveVariant from '#nextjs/utilities/deriveVariant'; import extractHeaders from '#server/utilities/extractHeaders'; import getPageData from '../../../../utilities/pageRequests/getPageData'; @@ -16,7 +15,6 @@ export const getServerSideProps: GetServerSideProps = async context => { ); const { headers: reqHeaders } = context.req; - const { isLite, isApp } = getPathExtension(context.resolvedUrl); const { id, @@ -42,10 +40,6 @@ export const getServerSideProps: GetServerSideProps = async context => { props: { error: data?.error || null, id, - isApp, - isLite, - isAmp: false, - isNextJs: true, pageData: pageData ? { ...pageData, From e3b41739ccb844eb237db432df23e71d62b71bca Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:25:25 +0000 Subject: [PATCH 24/91] Remove platform flag from `watch` page --- .../pages/[service]/watch/[id]/live/[[...variant]].page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx index 9e61886b256..95aeeb7a461 100644 --- a/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx @@ -63,7 +63,6 @@ export const getServerSideProps: GetServerSideProps = async context => { context.res.statusCode = data.status; const baseProps = { - isNextJs: true, status: data.status, pageType: LIVE_TV_PAGE as PageTypes, id, From b58cfff7dc622a4260ceda772bf62bd2d2f0992f Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:25:50 +0000 Subject: [PATCH 25/91] Remove platform flags from catch-all route --- ws-nextjs-app/pages/[service]/[[...]].page.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/[[...]].page.tsx b/ws-nextjs-app/pages/[service]/[[...]].page.tsx index e40e75c623a..482b0b75a0a 100644 --- a/ws-nextjs-app/pages/[service]/[[...]].page.tsx +++ b/ws-nextjs-app/pages/[service]/[[...]].page.tsx @@ -74,18 +74,12 @@ export const getServerSideProps: GetServerSideProps = async context => { return handleArticleRoute(context); } - const { isAmp, isApp, isLite } = getPathExtension(resolvedUrl); - logResponseTime({ path: context.resolvedUrl }, context.res, () => null); context.res.statusCode = 404; return { props: { - isApp, - isAmp, - isLite, - isNextJs: true, service, status: 404, timeOnServer: Date.now(), // TODO: check if needed? See https://github.com/bbc/simorgh/pull/10857/files#r1200274478 From b7ff18b44dbca6b0054ba434e98c626bdb8cc9ac Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:26:22 +0000 Subject: [PATCH 26/91] Remove platform flags from `wrapped` page --- .../pages/[service]/wrapped.page.tsx | 79 +++++++++---------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/wrapped.page.tsx b/ws-nextjs-app/pages/[service]/wrapped.page.tsx index 272d2fb701b..7b6d263481c 100644 --- a/ws-nextjs-app/pages/[service]/wrapped.page.tsx +++ b/ws-nextjs-app/pages/[service]/wrapped.page.tsx @@ -26,10 +26,9 @@ import { SERVER_SIDE_RENDER_REQUEST_RECEIVED, } from '#app/lib/logger.const'; import { Services, Variants } from '#models/types/global'; +import extractHeaders from '#server/utilities/extractHeaders'; import styles from './wrappedStyles'; -import extractHeaders from '../../../src/server/utilities/extractHeaders'; - interface PageDataParams extends ParsedUrlQuery { id: string; page?: string; @@ -82,8 +81,6 @@ export const getServerSideProps: GetServerSideProps = async context => { return { props: { error: null, - isAmp: false, - isNextJs: true, page: null, pageData: { metadata: { @@ -238,46 +235,44 @@ const pageLayout = () => { } }, []); return ( - <> -
-
-
-

- 2024 -

-
-

- -

-

- -

-
    -
  • - {' '} -
  • -
  • - {' '} -
  • -
-

- -

-
    -

    - -

    - -
+
+
+
+

+ 2024 +

+
+

+ +

+

+ +

+
    +
  • + {' '} +
  • +
  • + {' '} +
  • +
+

+ +

+
    +

    + +

    +
-
- +
+
); }; From 6470eaf7899c8a03ed584ef7c2f784f1b768ee27 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:26:42 +0000 Subject: [PATCH 27/91] Remove platform flags from `languages` page --- ws-nextjs-app/pages/ws/languages.page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ws-nextjs-app/pages/ws/languages.page.tsx b/ws-nextjs-app/pages/ws/languages.page.tsx index 8fcef8342ba..f74ac1f7f6b 100644 --- a/ws-nextjs-app/pages/ws/languages.page.tsx +++ b/ws-nextjs-app/pages/ws/languages.page.tsx @@ -17,8 +17,6 @@ export const getServerSideProps: GetServerSideProps = async context => { const baseProps = { error: null, - isAmp: false, - isNextJs: true, page: null, status: 200, timeOnServer: Date.now(), From 4dcc6565989931cd447686a844b0d1f285e392c3 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:27:46 +0000 Subject: [PATCH 28/91] Remove unneeded import --- ws-nextjs-app/pages/[service]/[[...]].page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ws-nextjs-app/pages/[service]/[[...]].page.tsx b/ws-nextjs-app/pages/[service]/[[...]].page.tsx index 482b0b75a0a..917d10a615d 100644 --- a/ws-nextjs-app/pages/[service]/[[...]].page.tsx +++ b/ws-nextjs-app/pages/[service]/[[...]].page.tsx @@ -3,7 +3,6 @@ import { GetServerSideProps } from 'next'; import dynamic from 'next/dynamic'; import logResponseTime from '#server/utilities/logResponseTime'; import extractHeaders from '#server/utilities/extractHeaders'; -import getPathExtension from '#app/utilities/getPathExtension'; import { AV_EMBEDS, ARTICLE_PAGE, From 49ffb5d8874c9148d0e89831d6c9edb4ee7c0526 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:31:32 +0000 Subject: [PATCH 29/91] Remove `extractHeaders` from articles route --- .../pages/[service]/articles/handleArticleRoute.test.ts | 8 -------- .../pages/[service]/articles/handleArticleRoute.ts | 3 --- 2 files changed, 11 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts index 361318ff908..253b5cf53ba 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.test.ts @@ -72,14 +72,10 @@ describe('handleArticleRoute', () => { expect(result).toEqual({ props: { - bbcOrigin: null, status: 500, - isUK: false, pageType: 'article', pathname: '/pidgin/articles/cvpde7nqj92o', service: 'pidgin', - showAdsBasedOnLocation: false, - showCookieBannerBasedOnCountry: true, timeOnServer: 1234567890000, variant: null, }, @@ -98,14 +94,10 @@ describe('handleArticleRoute', () => { expect(result).toEqual({ props: { - bbcOrigin: null, status: 404, - isUK: false, pageType: 'article', pathname: '/pidgin/articles/cvpde7nqj92o', service: 'pidgin', - showAdsBasedOnLocation: false, - showCookieBannerBasedOnCountry: true, timeOnServer: 1234567890000, variant: null, }, diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts index 6fb8a469c6e..67ff57a2a59 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts @@ -1,5 +1,4 @@ import { GetServerSidePropsContext } from 'next'; -import extractHeaders from '#server/utilities/extractHeaders'; import { ARTICLE_PAGE, MEDIA_ARTICLE_PAGE } from '#app/routes/utils/pageTypes'; import parseRoute from '#app/routes/utils/parseRoute'; import nodeLogger from '#lib/logger.node'; @@ -77,7 +76,6 @@ export default async (context: GetServerSidePropsContext) => { variant: variant || null, pageType: ARTICLE_PAGE, pathname: resolvedUrlWithoutQuery, - ...extractHeaders(reqHeaders), }, }; } @@ -143,7 +141,6 @@ export default async (context: GetServerSidePropsContext) => { service, status, variant: variant || null, - ...extractHeaders(reqHeaders), }, }; }; From 9abc4151371361299d1baed2fc56b98b2d2336a7 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:31:49 +0000 Subject: [PATCH 30/91] Remove `extractHeaders` from av-embeds route --- ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts b/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts index b0c3184bfbe..82d17d0f233 100644 --- a/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts +++ b/ws-nextjs-app/pages/[service]/av-embeds/handleAvRoute.ts @@ -1,5 +1,4 @@ import { GetServerSidePropsContext } from 'next'; -import extractHeaders from '#server/utilities/extractHeaders'; import { AV_EMBEDS } from '#app/routes/utils/pageTypes'; import fetchPageData from '#app/routes/utils/fetchPageData'; import certsRequired from '#app/routes/utils/certsRequired'; @@ -18,10 +17,7 @@ import getAgent from '#server/utilities/getAgent'; const logger = nodeLogger(__filename); export default async (context: GetServerSidePropsContext) => { - const { - resolvedUrl, - req: { headers: reqHeaders }, - } = context; + const { resolvedUrl } = context; let pageStatus; let pageJson; @@ -149,7 +145,6 @@ export default async (context: GetServerSidePropsContext) => { service, status: pageStatus, variant, - ...extractHeaders(reqHeaders), }, }; }; From f5212574c780b655ad08a03d4fffd08ff5691c8b Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:32:04 +0000 Subject: [PATCH 31/91] Remove `extractHeaders` from downloads route --- .../pages/[service]/downloads/[[...variant]].page.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx index 5182f7d1071..751995b41e3 100644 --- a/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/downloads/[[...variant]].page.tsx @@ -6,7 +6,6 @@ import logResponseTime from '#server/utilities/logResponseTime'; import deriveVariant from '#nextjs/utilities/deriveVariant'; import PageDataParams from '#app/models/types/pageDataParams'; import getToggles from '#app/lib/utilities/getToggles/withCache'; -import extractHeaders from '#server/utilities/extractHeaders'; import dataFetch from './dataFetch'; const downloadsPageLayout = dynamic(() => import('./downloadsPageLayout')); @@ -45,8 +44,6 @@ export const getServerSideProps: GetServerSideProps = async context => { const downloadData = await dataFetch(service); const toggles = await getToggles(service); - const { headers: reqHeaders } = context.req; - return { props: { error: null, @@ -65,7 +62,6 @@ export const getServerSideProps: GetServerSideProps = async context => { timeOnServer: Date.now(), // TODO: check if needed? toggles, variant, - ...extractHeaders(reqHeaders), }, }; }; From 898c0d37efbaaf7aaf07004d26a9c858addf23cd Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:32:22 +0000 Subject: [PATCH 32/91] Remove `extractHeaders` from live route --- .../pages/[service]/live/[id]/[[...variant]].page.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx index 08f72ba788f..4b9e21b5a79 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/[[...variant]].page.tsx @@ -10,7 +10,6 @@ import sendCustomMetric from '#server/utilities/customMetrics'; import { NON_200_RESPONSE } from '#server/utilities/customMetrics/metrics.const'; import PageDataParams from '#app/models/types/pageDataParams'; import deriveVariant from '#nextjs/utilities/deriveVariant'; -import extractHeaders from '#server/utilities/extractHeaders'; import isValidPageNumber from '#nextjs/utilities/pageQueryValidator'; import getPageData from '#nextjs/utilities/pageRequests/getPageData'; @@ -41,8 +40,6 @@ export const getServerSideProps: GetServerSideProps = async context => { post: assetId, } = context.query as PageDataParams; - const { headers: reqHeaders } = context.req; - const variant = deriveVariant(variantFromUrl); if (!isValidPageNumber(page)) { @@ -61,7 +58,6 @@ export const getServerSideProps: GetServerSideProps = async context => { status: 404, timeOnServer: Date.now(), variant, - ...extractHeaders(reqHeaders), }, }; } @@ -115,7 +111,6 @@ export const getServerSideProps: GetServerSideProps = async context => { status: data.status, timeOnServer: Date.now(), // TODO: check if needed? variant, - ...extractHeaders(reqHeaders), }, }; }; From 6be2b709bc2cccc85bb7243b3af333fc59066281 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:32:36 +0000 Subject: [PATCH 33/91] Remove `extractHeaders` from send route --- .../pages/[service]/send/[id]/[[...variant]].page.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx index f435454f87f..07288e83f77 100644 --- a/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/send/[id]/[[...variant]].page.tsx @@ -3,7 +3,6 @@ import dynamic from 'next/dynamic'; import PageDataParams from '#models/types/pageDataParams'; import { UGC_PAGE } from '#app/routes/utils/pageTypes'; import deriveVariant from '#nextjs/utilities/deriveVariant'; -import extractHeaders from '#server/utilities/extractHeaders'; import getPageData from '../../../../utilities/pageRequests/getPageData'; const UGCPageLayout = dynamic(() => import('./UGCPageLayout')); @@ -14,8 +13,6 @@ export const getServerSideProps: GetServerSideProps = async context => { 'public, stale-if-error=300, stale-while-revalidate=120, max-age=30', ); - const { headers: reqHeaders } = context.req; - const { id, service, @@ -55,7 +52,6 @@ export const getServerSideProps: GetServerSideProps = async context => { status: status ?? 500, variant, timeOnServer: Date.now(), // TODO: check if needed? - ...extractHeaders(reqHeaders), }, }; }; From a03480524bc665b8df6c0aabf2c706b8ad64c499 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:32:52 +0000 Subject: [PATCH 34/91] Remove `extractHeaders` from watch route --- .../pages/[service]/watch/[id]/live/[[...variant]].page.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx index 95aeeb7a461..a7c98ee0ef8 100644 --- a/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/watch/[id]/live/[[...variant]].page.tsx @@ -9,7 +9,6 @@ import getPageData from '#nextjs/utilities/pageRequests/getPageData'; import logResponseTime from '#src/server/utilities/logResponseTime'; import { OK } from '#app/lib/statusCodes.const'; import { ROUTING_INFORMATION } from '#app/lib/logger.const'; -import extractHeaders from '#src/server/utilities/extractHeaders'; const LiveTvLayout = dynamic(() => import('./LiveTvPageLayout')); @@ -38,8 +37,6 @@ export const getServerSideProps: GetServerSideProps = async context => { const variant = deriveVariant(variantFromUrl); - const { headers: reqHeaders } = context.req; - const { data } = await getPageData({ id, service, @@ -82,7 +79,6 @@ export const getServerSideProps: GetServerSideProps = async context => { } : null, pathname: context?.resolvedUrl, - ...extractHeaders(reqHeaders), }; return { props: { From a750b0c74e34cdef021266a495c0ccc99be054af Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:33:08 +0000 Subject: [PATCH 35/91] Remove `extractHeaders` from catch-all route --- ws-nextjs-app/pages/[service]/[[...]].page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/[[...]].page.tsx b/ws-nextjs-app/pages/[service]/[[...]].page.tsx index 917d10a615d..313d21d3937 100644 --- a/ws-nextjs-app/pages/[service]/[[...]].page.tsx +++ b/ws-nextjs-app/pages/[service]/[[...]].page.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { GetServerSideProps } from 'next'; import dynamic from 'next/dynamic'; import logResponseTime from '#server/utilities/logResponseTime'; -import extractHeaders from '#server/utilities/extractHeaders'; import { AV_EMBEDS, ARTICLE_PAGE, @@ -83,7 +82,6 @@ export const getServerSideProps: GetServerSideProps = async context => { status: 404, timeOnServer: Date.now(), // TODO: check if needed? See https://github.com/bbc/simorgh/pull/10857/files#r1200274478 variant, - ...extractHeaders(reqHeaders), }, }; }; From 4575d98a192e1d7846f8815690b3988b97bf789b Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:33:24 +0000 Subject: [PATCH 36/91] Remove `extractHeaders` from wrapped route --- ws-nextjs-app/pages/[service]/wrapped.page.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/wrapped.page.tsx b/ws-nextjs-app/pages/[service]/wrapped.page.tsx index 7b6d263481c..09c27c728a8 100644 --- a/ws-nextjs-app/pages/[service]/wrapped.page.tsx +++ b/ws-nextjs-app/pages/[service]/wrapped.page.tsx @@ -26,7 +26,6 @@ import { SERVER_SIDE_RENDER_REQUEST_RECEIVED, } from '#app/lib/logger.const'; import { Services, Variants } from '#models/types/global'; -import extractHeaders from '#server/utilities/extractHeaders'; import styles from './wrappedStyles'; interface PageDataParams extends ParsedUrlQuery { @@ -92,7 +91,6 @@ export const getServerSideProps: GetServerSideProps = async context => { service, status: 200, timeOnServer: Date.now(), // TODO: check if needed? - ...extractHeaders(reqHeaders), }, }; }; From fc981a247fee37373a389434d2a95345401ae1b2 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 18:51:51 +0000 Subject: [PATCH 37/91] Remove duplicate `ctx.res?.setHeader` --- ws-nextjs-app/utilities/cspHeaderResponse/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index f54e2d33446..1b310051c92 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -84,11 +84,6 @@ const cspHeaderResponse = async ({ ...BUMP4SpecificConditions, }); - ctx.res?.setHeader( - 'Content-Security-Policy', - contentSecurityPolicyHeaderValue, - ); - ctx.res?.setHeader( 'report-to', JSON.stringify({ From 1ad3a42ecab32432d57743d1b1277995e756fddf Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 20:32:10 +0000 Subject: [PATCH 38/91] Move logic from _document and move to _app --- ws-nextjs-app/pages/_app.page.tsx | 59 ++++++++++++++++++- ws-nextjs-app/pages/_document.page.tsx | 51 +--------------- .../utilities/cspHeaderResponse/index.test.ts | 9 +-- .../utilities/cspHeaderResponse/index.ts | 4 +- 4 files changed, 65 insertions(+), 58 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 5250c0fb8fc..d67943b3c99 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import type { AppProps } from 'next/app'; +import NextApp, { AppContext, AppProps } from 'next/app'; +import { NextPageContext } from 'next'; import { ATIData } from '#app/components/ATIAnalytics/types'; import ThemeProvider from '#app/components/ThemeProvider'; import { ToggleContextProvider } from '#app/contexts/ToggleContext'; @@ -16,6 +17,11 @@ import { ServiceContextProvider } from '#app/contexts/ServiceContext'; import { RequestContextProvider } from '#app/contexts/RequestContext'; import { EventTrackingContextProvider } from '#app/contexts/EventTrackingContext'; import { UserContextProvider } from '#app/contexts/UserContext'; +import extractHeaders from '#src/server/utilities/extractHeaders'; +import getToggles from '#app/lib/utilities/getToggles'; +import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; +import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; +import getPathExtension from '#app/utilities/getPathExtension'; interface Props extends AppProps { pageProps: { @@ -128,3 +134,54 @@ export default function App({ Component, pageProps }: Props) { ); } + +const addServiceChainAndCspHeaders = async ({ + ctx, + toggles, +}: { + ctx: NextPageContext; + toggles: Toggles; +}) => { + ctx.res?.setHeader( + 'req-svc-chain', + addPlatformToRequestChainHeader({ + headers: ctx.req?.headers as unknown as Headers, + }), + ); + + const hostname = ctx.req?.headers.host || ''; + const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; + + const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); + + const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; + + if (PRODUCTION_ONLY) { + await cspHeaderResponse({ ctx, toggles }); + } +}; + +App.getInitialProps = async (appContext: AppContext) => { + const appProps = await NextApp.getInitialProps(appContext); + const { req, asPath } = appContext.ctx; + + const toggles = await getToggles(); + + await addServiceChainAndCspHeaders({ ctx: appContext.ctx, toggles }); + + const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); + + return { + ...appProps, + pageProps: { + ...appProps.pageProps, + // These are props passed down to ALL pages + ...extractHeaders(req?.headers || {}), + isApp, + isAmp, + isLite, + isNextJs: true, + toggles, + }, + }; +}; diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index d674c9b0bf3..6e831935602 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -35,13 +35,9 @@ import NO_JS_CLASSNAME from '#app/lib/noJs.const'; import getPathExtension from '#app/utilities/getPathExtension'; import ReverbTemplate from '#src/server/Document/Renderers/ReverbTemplate'; -import { PageTypes, Toggles } from '#app/models/types/global'; +import { PageTypes } from '#app/models/types/global'; import ComponentTracking from '#src/server/Document/Renderers/ComponentTracking'; import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript'; -import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; -import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; -import getToggles from '#app/lib/utilities/getToggles/withCache'; -import extractHeaders from '#server/utilities/extractHeaders'; import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders'; import derivePageType from '../utilities/derivePageType'; @@ -87,33 +83,6 @@ const handleServerLogging = ({ } }; -const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; - -const addServiceChainAndCspHeaders = async ({ - ctx, - toggles, -}: { - ctx: DocumentContext; - toggles: Toggles; -}) => { - ctx.res?.setHeader( - 'req-svc-chain', - addPlatformToRequestChainHeader({ - headers: ctx.req?.headers as unknown as Headers, - }), - ); - - const hostname = ctx.req?.headers.host || ''; - - const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); - - const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; - - if (PRODUCTION_ONLY) { - await cspHeaderResponse({ ctx, toggles }); - } -}; - type DocProps = { clientSideEnvVariables: EnvConfig; css: string; @@ -134,10 +103,6 @@ export default class AppDocument extends Document { const { isApp, isAmp, isLite } = getPathExtension(url); - const toggles = await getToggles(); - - await addServiceChainAndCspHeaders({ ctx, toggles }); - const cache = createCache({ key: 'css' }); const { extractCritical } = createEmotionServer(cache); @@ -146,19 +111,7 @@ export default class AppDocument extends Document { originalRenderPage({ enhanceApp: App => props => ( - + ), }); diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index 1095422081d..75bba36f0c9 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -1,10 +1,7 @@ -import { DocumentContext } from 'next/document'; +import { NextPageContext } from 'next'; import cspHeaderResponse from '.'; -const createDocumentContext = ( - pathname: string, - country?: string, -): DocumentContext => { +const createDocumentContext = (pathname: string, country?: string) => { const url = new URL(`https://www.test.bbc.com${pathname}`); const headers = new Headers({ 'x-country': `${country}` }); @@ -17,7 +14,7 @@ const createDocumentContext = ( getHeader: jest.fn(), setHeader: jest.fn(), }, - } as unknown as DocumentContext; + } as unknown as NextPageContext; }; const policies = [ diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index 1b310051c92..e7feab328e2 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -3,7 +3,7 @@ import getPathExtension from '#app/utilities/getPathExtension'; import isLiveEnv from '#lib/utilities/isLive'; import { Services, Toggles } from '#app/models/types/global'; import SERVICES from '#app/lib/config/services'; -import { DocumentContext } from 'next/document'; +import { NextPageContext } from 'next'; const directiveToString = (directives: Record) => { const map = new Map(Object.entries(directives)); @@ -44,7 +44,7 @@ const cspHeaderResponse = async ({ ctx, toggles, }: { - ctx: DocumentContext; + ctx: NextPageContext; toggles: Toggles; }) => { const reqUrl = ctx.req?.url || ''; From d7fa9ebe808bfaaed6bf1ea9ebeaa3d91a2081a5 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 21:22:03 +0000 Subject: [PATCH 39/91] Fix type imports --- ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts | 2 +- ws-nextjs-app/utilities/cspHeaderResponse/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index 75bba36f0c9..eec3588cce5 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -1,4 +1,4 @@ -import { NextPageContext } from 'next'; +import { NextPageContext } from 'next/types'; import cspHeaderResponse from '.'; const createDocumentContext = (pathname: string, country?: string) => { diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index e7feab328e2..ac47c1a3e0f 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -1,9 +1,9 @@ +import { NextPageContext } from 'next/types'; import { cspDirectives } from '#server/utilities/cspHeader/directives'; import getPathExtension from '#app/utilities/getPathExtension'; import isLiveEnv from '#lib/utilities/isLive'; import { Services, Toggles } from '#app/models/types/global'; import SERVICES from '#app/lib/config/services'; -import { NextPageContext } from 'next'; const directiveToString = (directives: Record) => { const map = new Map(Object.entries(directives)); From 5910d7dc7907550aa0b556c0f3bebdc3a468a9ee Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 2 Dec 2025 21:22:29 +0000 Subject: [PATCH 40/91] Remove `appProps` and add comment --- ws-nextjs-app/pages/_app.page.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index d67943b3c99..8666a9897a4 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import NextApp, { AppContext, AppProps } from 'next/app'; -import { NextPageContext } from 'next'; +import { AppContext, AppProps } from 'next/app'; +import { NextPageContext } from 'next/types'; import { ATIData } from '#app/components/ATIAnalytics/types'; import ThemeProvider from '#app/components/ThemeProvider'; import { ToggleContextProvider } from '#app/contexts/ToggleContext'; @@ -150,6 +150,7 @@ const addServiceChainAndCspHeaders = async ({ ); const hostname = ctx.req?.headers.host || ''; + const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); @@ -161,21 +162,19 @@ const addServiceChainAndCspHeaders = async ({ } }; -App.getInitialProps = async (appContext: AppContext) => { - const appProps = await NextApp.getInitialProps(appContext); - const { req, asPath } = appContext.ctx; +App.getInitialProps = async ({ ctx }: AppContext) => { + const { req, asPath } = ctx; const toggles = await getToggles(); - await addServiceChainAndCspHeaders({ ctx: appContext.ctx, toggles }); + await addServiceChainAndCspHeaders({ ctx, toggles }); const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); return { - ...appProps, pageProps: { - ...appProps.pageProps, - // These are props passed down to ALL pages + // These are props passed down to ALL pages and merged with page + // specific props from getInitialProps / getServerSideProps ...extractHeaders(req?.headers || {}), isApp, isAmp, From 3f88c1b489da9b5b130a8fbae527a553490676cc Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 3 Dec 2025 09:12:20 +0000 Subject: [PATCH 41/91] Use cached version of toggles --- ws-nextjs-app/pages/_app.page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 8666a9897a4..fa0cd0ac570 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -18,7 +18,7 @@ import { RequestContextProvider } from '#app/contexts/RequestContext'; import { EventTrackingContextProvider } from '#app/contexts/EventTrackingContext'; import { UserContextProvider } from '#app/contexts/UserContext'; import extractHeaders from '#src/server/utilities/extractHeaders'; -import getToggles from '#app/lib/utilities/getToggles'; +import getToggles from '#app/lib/utilities/getToggles/withCache'; import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; import getPathExtension from '#app/utilities/getPathExtension'; From 11bcf52df1b5e143ea00267659c333ded1ed13cc Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Thu, 4 Dec 2025 12:16:48 +0000 Subject: [PATCH 42/91] Rename CSP header tests to be more reflective of intent --- ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index eec3588cce5..42fba6fcc9f 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -57,7 +57,7 @@ describe('shouldServeRelaxedCsp', () => { const expectedFullCsp = "default-src 'self' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.googlesyndication.com;child-src 'self';connect-src https:;font-src *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com data: https://*.teads.tv https://cdnjs.cloudflare.com/ajax/libs/font-awesome/ https://fonts.gstatic.com;frame-src 'self' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.amazon-adsystem.com https://*.chartbeat.com https://*.doubleclick.net https://*.facebook.com https://*.google.com https://*.googleadservices.com https://*.googlesyndication.com https://*.mapcreator.io https://*.teads.tv https://*.thomsonreuters.com https://*.twitter.com https://bbc-maps.carto.com https://bbc.com https://cdn.privacy-mgmt.com https://chartbeat.com https://edigitalsurvey.com https://flo.uri.sh https://public.flourish.studio https://www.instagram.com https://www.riddle.com https://www.tiktok.com https://www.youtube-nocookie.com https://www.youtube.com;img-src *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com data: 'self' http://ping.chartbeat.net https://*.adsafeprotected.com https://*.amazon-adsystem.com https://*.cdninstagram.com https://*.doubleclick.net https://*.effectivemeasure.net https://*.google.com https://*.googlesyndication.com https://*.googleusercontent.com https://*.gstatic.com https://*.imrworldwide.com https://*.teads.tv https://*.tiktokcdn.com https://*.twimg.com https://*.twitter.com https://*.xx.fbcdn.net https://i.ytimg.com https://logws1363.ati-host.net https://ping.chartbeat.net https://sb.scorecardresearch.com https://www.googleadservices.com;script-src 'self' 'unsafe-eval' 'unsafe-inline' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com http://*.chartbeat.com http://localhost:1124 http://localhost:7080 https://*.adsafeprotected.com https://*.amazon-adsystem.com https://*.chartbeat.com https://*.covatic.io https://*.doubleverify.com https://*.effectivemeasure.net https://*.facebook.com https://*.g.doubleclick.net https://*.google.ae https://*.google.at https://*.google.az https://*.google.be https://*.google.ca https://*.google.ch https://*.google.cl https://*.google.co.id https://*.google.co.il https://*.google.co.in https://*.google.co.jp https://*.google.co.kr https://*.google.co.nz https://*.google.co.tz https://*.google.co.ve https://*.google.com https://*.google.com.af https://*.google.com.ar https://*.google.com.au https://*.google.com.bo https://*.google.com.br https://*.google.com.co https://*.google.com.cy https://*.google.com.ec https://*.google.com.eg https://*.google.com.gt https://*.google.com.hk https://*.google.com.kh https://*.google.com.mm https://*.google.com.mt https://*.google.com.mx https://*.google.com.ng https://*.google.com.ni https://*.google.com.np https://*.google.com.pe https://*.google.com.pk https://*.google.com.pr https://*.google.com.py https://*.google.com.ro https://*.google.com.sa https://*.google.com.sg https://*.google.com.sv https://*.google.com.tr https://*.google.com.tw https://*.google.com.ua https://*.google.com.uy https://*.google.com.vn https://*.google.cv https://*.google.cz https://*.google.de https://*.google.dk https://*.google.es https://*.google.fi https://*.google.fr https://*.google.ge https://*.google.hn https://*.google.ie https://*.google.iq https://*.google.it https://*.google.jo https://*.google.kz https://*.google.lk https://*.google.lv https://*.google.nl https://*.google.no https://*.google.pl https://*.google.ru https://*.google.se https://*.google.so https://*.googlesyndication.com https://*.imrworldwide.com https://*.mapcreator.io https://*.permutive.com https://*.teads.tv https://*.thomsonreuters.com https://*.twimg.com https://*.twitter.com https://*.webcontentassessor.com https://*.xx.fbcdn.net https://adservice.google.co.uk https://bbc.gscontxt.net https://cdn.ampproject.org https://cdn.privacy-mgmt.com https://connect.facebook.net https://lf16-tiktok-web.ttwstatic.com https://public.flourish.studio https://sb.scorecardresearch.com https://www.googletagservices.com https://www.instagram.com https://www.riddle.com https://www.tiktok.com;style-src 'unsafe-inline' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.twimg.com https://*.twitter.com https://*.xx.fbcdn.net https://fonts.googleapis.com https://lf16-tiktok-web.ttwstatic.com;media-src https: blob:;worker-src blob: 'self' *.bbc.co.uk *.bbc.com;report-to worldsvc;upgrade-insecure-requests;"; - it('returns true when toggle is enabled but does not have values set', async () => { + it('returns "relaxed" Csp when toggle is enabled but does not have values set', async () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); await cspHeaderResponse({ @@ -74,7 +74,7 @@ describe('shouldServeRelaxedCsp', () => { expect(requestCsp).toEqual(expectedRelaxedCsp); }); - it('returns true when country is not in omittedCountries', async () => { + it('returns "relaxed" Csp when country is not in omittedCountries', async () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'ax'); await cspHeaderResponse({ @@ -91,7 +91,7 @@ describe('shouldServeRelaxedCsp', () => { expect(requestCsp).toEqual(expectedRelaxedCsp); }); - it('returns false when toggle is enabled and given country is in omittedCountries', async () => { + it('returns "full" CSP when toggle is enabled and given country is in omittedCountries', async () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); await cspHeaderResponse({ @@ -108,7 +108,7 @@ describe('shouldServeRelaxedCsp', () => { expect(requestCsp).toEqual(expectedFullCsp); }); - it('returns false when adsNonce.enabled is false', async () => { + it('returns "full" CSP when adsNonce.enabled is false', async () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); await cspHeaderResponse({ From dcf3ade481745e5e64ebcd9e5b055fa9f7ee0aea Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Thu, 4 Dec 2025 14:38:37 +0000 Subject: [PATCH 43/91] Make toggles destruct more readable --- ws-nextjs-app/utilities/cspHeaderResponse/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index ac47c1a3e0f..a41f0ed2e64 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -55,9 +55,10 @@ const cspHeaderResponse = async ({ let countryList = ''; if (isValidService(reqUrl)) { - ({ enabled: hasAdsScripts, value: countryList = '' } = - // @ts-expect-error- Toggles type issue - toggles?.adsNonce || { enabled: false, value: '' }); + // @ts-expect-error - Toggles type issue + const adsNonceToggle = toggles?.adsNonce || { enabled: false, value: '' }; + hasAdsScripts = adsNonceToggle.enabled; + countryList = adsNonceToggle.value; } const country = From 052b96c43ccb9a6192abf02f467f169ea84b9c8e Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 9 Dec 2025 17:00:27 +0000 Subject: [PATCH 44/91] Add `service` to toggles fetch from `ctx` --- ws-nextjs-app/pages/_app.page.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 64c6469452b..70c5756829f 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -162,9 +162,13 @@ const addServiceChainAndCspHeaders = async ({ }; App.getInitialProps = async ({ ctx }: AppContext) => { - const { req, asPath } = ctx; + const { + req, + asPath, + query: { service }, + } = ctx; - const toggles = await getToggles(); + const toggles = await getToggles(service); await addServiceChainAndCspHeaders({ ctx, toggles }); From 1d92695799608528374f73c2386f46a06ee091a1 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 9 Dec 2025 17:12:33 +0000 Subject: [PATCH 45/91] Pass `service` down to csp header function to simplify logic --- ws-nextjs-app/pages/_app.page.tsx | 17 +++++++++-------- .../utilities/cspHeaderResponse/index.test.ts | 6 +++++- .../utilities/cspHeaderResponse/index.ts | 15 +++++++-------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 70c5756829f..97e4d6eaeea 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -19,8 +19,11 @@ import { UserContextProvider } from '#app/contexts/UserContext'; import extractHeaders from '#src/server/utilities/extractHeaders'; import getToggles from '#app/lib/utilities/getToggles/withCache'; import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; -import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; +import cspHeaderResponse, { + CspHeaderResponseProps, +} from '#nextjs/utilities/cspHeaderResponse'; import getPathExtension from '#app/utilities/getPathExtension'; +import PageDataParams from '#app/models/types/pageDataParams'; interface Props extends AppProps { pageProps: { @@ -136,11 +139,9 @@ export default function App({ Component, pageProps }: Props) { const addServiceChainAndCspHeaders = async ({ ctx, + service, toggles, -}: { - ctx: NextPageContext; - toggles: Toggles; -}) => { +}: CspHeaderResponseProps) => { ctx.res?.setHeader( 'req-svc-chain', addPlatformToRequestChainHeader({ @@ -157,7 +158,7 @@ const addServiceChainAndCspHeaders = async ({ const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; if (PRODUCTION_ONLY) { - await cspHeaderResponse({ ctx, toggles }); + await cspHeaderResponse({ ctx, service, toggles }); } }; @@ -166,11 +167,11 @@ App.getInitialProps = async ({ ctx }: AppContext) => { req, asPath, query: { service }, - } = ctx; + } = ctx as NextPageContext & { query: PageDataParams }; const toggles = await getToggles(service); - await addServiceChainAndCspHeaders({ ctx, toggles }); + await addServiceChainAndCspHeaders({ ctx, service, toggles }); const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index 42fba6fcc9f..48c95c1d93b 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -36,7 +36,7 @@ describe('cspHeaderResponse', () => { it.each(policies)('should set %s in the request CSP', async policy => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - await cspHeaderResponse({ ctx, toggles: {} }); + await cspHeaderResponse({ ctx, service: 'pidgin', toggles: {} }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', @@ -62,6 +62,7 @@ describe('shouldServeRelaxedCsp', () => { await cspHeaderResponse({ ctx, + service: 'pidgin', toggles: { adsNonce: { enabled: true, value: '' }, }, @@ -79,6 +80,7 @@ describe('shouldServeRelaxedCsp', () => { await cspHeaderResponse({ ctx, + service: 'pidgin', toggles: { adsNonce: { enabled: true, value: 'gb' }, }, @@ -96,6 +98,7 @@ describe('shouldServeRelaxedCsp', () => { await cspHeaderResponse({ ctx, + service: 'pidgin', toggles: { adsNonce: { enabled: true, value: 'gb,es' }, }, @@ -113,6 +116,7 @@ describe('shouldServeRelaxedCsp', () => { await cspHeaderResponse({ ctx, + service: 'pidgin', toggles: { adsNonce: { enabled: false, value: '' } }, }); diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index a41f0ed2e64..21f8ba7fd3e 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -35,18 +35,17 @@ const isRelaxedCspEnabled = ( return !omittedCountriesList.includes(country.toLowerCase()); }; -const isValidService = (str: string) => { - const [service] = str.split('/').filter(Boolean) as [Services?]; - return service && SERVICES.includes(service); +export type CspHeaderResponseProps = { + ctx: NextPageContext; + service: Services; + toggles: Toggles; }; const cspHeaderResponse = async ({ ctx, + service, toggles, -}: { - ctx: NextPageContext; - toggles: Toggles; -}) => { +}: CspHeaderResponseProps) => { const reqUrl = ctx.req?.url || ''; const { isAmp } = getPathExtension(reqUrl); const isLive = isLiveEnv(); @@ -54,7 +53,7 @@ const cspHeaderResponse = async ({ let hasAdsScripts = false; let countryList = ''; - if (isValidService(reqUrl)) { + if (SERVICES.includes(service)) { // @ts-expect-error - Toggles type issue const adsNonceToggle = toggles?.adsNonce || { enabled: false, value: '' }; hasAdsScripts = adsNonceToggle.enabled; From d1ee5219161fe0f9d3016d74f9efc6e73fc4a307 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 9 Dec 2025 17:37:36 +0000 Subject: [PATCH 46/91] Move comment for clarity --- ws-nextjs-app/pages/_app.page.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 97e4d6eaeea..f6862e4442c 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -162,6 +162,9 @@ const addServiceChainAndCspHeaders = async ({ } }; +// This runs on the server before rendering the page. +// The props returned are passed down to ALL pages and merged with page +// specific props from getInitialProps / getServerSideProps App.getInitialProps = async ({ ctx }: AppContext) => { const { req, @@ -169,16 +172,14 @@ App.getInitialProps = async ({ ctx }: AppContext) => { query: { service }, } = ctx as NextPageContext & { query: PageDataParams }; + const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); + const toggles = await getToggles(service); await addServiceChainAndCspHeaders({ ctx, service, toggles }); - const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); - return { pageProps: { - // These are props passed down to ALL pages and merged with page - // specific props from getInitialProps / getServerSideProps ...extractHeaders(req?.headers || {}), isApp, isAmp, From a35e9822b92adc8533e5f68df1273475e0df2f11 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 9 Dec 2025 17:44:03 +0000 Subject: [PATCH 47/91] Get `service` from path instead of `query` --- ws-nextjs-app/pages/_app.page.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index f6862e4442c..e458ac936e8 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -23,7 +23,6 @@ import cspHeaderResponse, { CspHeaderResponseProps, } from '#nextjs/utilities/cspHeaderResponse'; import getPathExtension from '#app/utilities/getPathExtension'; -import PageDataParams from '#app/models/types/pageDataParams'; interface Props extends AppProps { pageProps: { @@ -166,14 +165,14 @@ const addServiceChainAndCspHeaders = async ({ // The props returned are passed down to ALL pages and merged with page // specific props from getInitialProps / getServerSideProps App.getInitialProps = async ({ ctx }: AppContext) => { - const { - req, - asPath, - query: { service }, - } = ctx as NextPageContext & { query: PageDataParams }; + const { req, asPath } = ctx as NextPageContext; const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); + const routeSegments = asPath?.split('/')?.filter(Boolean); + + const [service] = (routeSegments || []) as [Services]; + const toggles = await getToggles(service); await addServiceChainAndCspHeaders({ ctx, service, toggles }); From 6a88bec4e3cdfa5ab35fb47f5c0aa46e9d0115e0 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 10 Dec 2025 11:33:37 +0000 Subject: [PATCH 48/91] Update augmentWithDisclaimer.test.ts --- .../[service]/articles/augmentWithDisclaimer.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/articles/augmentWithDisclaimer.test.ts b/ws-nextjs-app/pages/[service]/articles/augmentWithDisclaimer.test.ts index 27a4a107f9b..790a7588002 100644 --- a/ws-nextjs-app/pages/[service]/articles/augmentWithDisclaimer.test.ts +++ b/ws-nextjs-app/pages/[service]/articles/augmentWithDisclaimer.test.ts @@ -37,14 +37,6 @@ describe('augmentWithDisclaimer', () => { expect(transformedData.content.model.blocks[0].type).toEqual('disclaimer'); }); - it('Should not add a disclaimer when toggled off for that service', () => { - const transformedData = transformer({ - positionFromTimestamp: 0, - })(buildPageDataFixture([])) as Article; - - expect(transformedData.content.model.blocks[0]).toBeUndefined(); - }); - it('Should not add a disclaimer when the article is a SFV', () => { const sfvArticle = { ...buildPageDataFixture([{ type: 'timestamp' }]), From 853616b92bc02bdeacac9c271cb2bf03392a0d7f Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 10 Dec 2025 15:35:56 +0000 Subject: [PATCH 49/91] Remove toggles fetch and common props from Offline page --- ws-nextjs-app/pages/[service]/offline/index.page.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/offline/index.page.tsx b/ws-nextjs-app/pages/[service]/offline/index.page.tsx index ea3d22f731a..fd7d627394e 100644 --- a/ws-nextjs-app/pages/[service]/offline/index.page.tsx +++ b/ws-nextjs-app/pages/[service]/offline/index.page.tsx @@ -5,7 +5,6 @@ import PageDataParams from '#app/models/types/pageDataParams'; import deriveVariant from '#nextjs/utilities/deriveVariant'; import extractHeaders from '#server/utilities/extractHeaders'; import logResponseTime from '#server/utilities/logResponseTime'; -import getToggles from '#app/lib/utilities/getToggles/withCache'; const OfflinePage = dynamic(() => import('./OfflinePage')); @@ -20,16 +19,11 @@ export const getServerSideProps: GetServerSideProps = async context => { 'public, max-age=300, stale-while-revalidate=600, stale-if-error=3600', ); - const toggles = await getToggles(service); - return { props: { service, variant, - toggles, pageType: OFFLINE_PAGE, - isNextJs: true, - isAmp: false, status: 200, timeOnServer: Date.now(), pathname: `/${service}/offline`, From 1af854689bbc7fcb711444e9dccef3446086505d Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 17:53:02 +0000 Subject: [PATCH 50/91] Remove `async/await` from header setting --- ws-nextjs-app/pages/_app.page.tsx | 6 +++--- .../utilities/cspHeaderResponse/index.test.ts | 20 +++++++++---------- .../utilities/cspHeaderResponse/index.ts | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index e458ac936e8..f7a0032b2fd 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -136,7 +136,7 @@ export default function App({ Component, pageProps }: Props) { ); } -const addServiceChainAndCspHeaders = async ({ +const addServiceChainAndCspHeaders = ({ ctx, service, toggles, @@ -157,7 +157,7 @@ const addServiceChainAndCspHeaders = async ({ const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; if (PRODUCTION_ONLY) { - await cspHeaderResponse({ ctx, service, toggles }); + cspHeaderResponse({ ctx, service, toggles }); } }; @@ -175,7 +175,7 @@ App.getInitialProps = async ({ ctx }: AppContext) => { const toggles = await getToggles(service); - await addServiceChainAndCspHeaders({ ctx, service, toggles }); + addServiceChainAndCspHeaders({ ctx, service, toggles }); return { pageProps: { diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index 48c95c1d93b..72100ff88ca 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -33,10 +33,10 @@ const policies = [ ]; describe('cspHeaderResponse', () => { - it.each(policies)('should set %s in the request CSP', async policy => { + it.each(policies)('should set %s in the request CSP', policy => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - await cspHeaderResponse({ ctx, service: 'pidgin', toggles: {} }); + cspHeaderResponse({ ctx, service: 'pidgin', toggles: {} }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', @@ -57,10 +57,10 @@ describe('shouldServeRelaxedCsp', () => { const expectedFullCsp = "default-src 'self' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.googlesyndication.com;child-src 'self';connect-src https:;font-src *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com data: https://*.teads.tv https://cdnjs.cloudflare.com/ajax/libs/font-awesome/ https://fonts.gstatic.com;frame-src 'self' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.amazon-adsystem.com https://*.chartbeat.com https://*.doubleclick.net https://*.facebook.com https://*.google.com https://*.googleadservices.com https://*.googlesyndication.com https://*.mapcreator.io https://*.teads.tv https://*.thomsonreuters.com https://*.twitter.com https://bbc-maps.carto.com https://bbc.com https://cdn.privacy-mgmt.com https://chartbeat.com https://edigitalsurvey.com https://flo.uri.sh https://public.flourish.studio https://www.instagram.com https://www.riddle.com https://www.tiktok.com https://www.youtube-nocookie.com https://www.youtube.com;img-src *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com data: 'self' http://ping.chartbeat.net https://*.adsafeprotected.com https://*.amazon-adsystem.com https://*.cdninstagram.com https://*.doubleclick.net https://*.effectivemeasure.net https://*.google.com https://*.googlesyndication.com https://*.googleusercontent.com https://*.gstatic.com https://*.imrworldwide.com https://*.teads.tv https://*.tiktokcdn.com https://*.twimg.com https://*.twitter.com https://*.xx.fbcdn.net https://i.ytimg.com https://logws1363.ati-host.net https://ping.chartbeat.net https://sb.scorecardresearch.com https://www.googleadservices.com;script-src 'self' 'unsafe-eval' 'unsafe-inline' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com http://*.chartbeat.com http://localhost:1124 http://localhost:7080 https://*.adsafeprotected.com https://*.amazon-adsystem.com https://*.chartbeat.com https://*.covatic.io https://*.doubleverify.com https://*.effectivemeasure.net https://*.facebook.com https://*.g.doubleclick.net https://*.google.ae https://*.google.at https://*.google.az https://*.google.be https://*.google.ca https://*.google.ch https://*.google.cl https://*.google.co.id https://*.google.co.il https://*.google.co.in https://*.google.co.jp https://*.google.co.kr https://*.google.co.nz https://*.google.co.tz https://*.google.co.ve https://*.google.com https://*.google.com.af https://*.google.com.ar https://*.google.com.au https://*.google.com.bo https://*.google.com.br https://*.google.com.co https://*.google.com.cy https://*.google.com.ec https://*.google.com.eg https://*.google.com.gt https://*.google.com.hk https://*.google.com.kh https://*.google.com.mm https://*.google.com.mt https://*.google.com.mx https://*.google.com.ng https://*.google.com.ni https://*.google.com.np https://*.google.com.pe https://*.google.com.pk https://*.google.com.pr https://*.google.com.py https://*.google.com.ro https://*.google.com.sa https://*.google.com.sg https://*.google.com.sv https://*.google.com.tr https://*.google.com.tw https://*.google.com.ua https://*.google.com.uy https://*.google.com.vn https://*.google.cv https://*.google.cz https://*.google.de https://*.google.dk https://*.google.es https://*.google.fi https://*.google.fr https://*.google.ge https://*.google.hn https://*.google.ie https://*.google.iq https://*.google.it https://*.google.jo https://*.google.kz https://*.google.lk https://*.google.lv https://*.google.nl https://*.google.no https://*.google.pl https://*.google.ru https://*.google.se https://*.google.so https://*.googlesyndication.com https://*.imrworldwide.com https://*.mapcreator.io https://*.permutive.com https://*.teads.tv https://*.thomsonreuters.com https://*.twimg.com https://*.twitter.com https://*.webcontentassessor.com https://*.xx.fbcdn.net https://adservice.google.co.uk https://bbc.gscontxt.net https://cdn.ampproject.org https://cdn.privacy-mgmt.com https://connect.facebook.net https://lf16-tiktok-web.ttwstatic.com https://public.flourish.studio https://sb.scorecardresearch.com https://www.googletagservices.com https://www.instagram.com https://www.riddle.com https://www.tiktok.com;style-src 'unsafe-inline' *.bbc.co.uk *.bbc.com *.bbci.co.uk *.bbci.com https://*.twimg.com https://*.twitter.com https://*.xx.fbcdn.net https://fonts.googleapis.com https://lf16-tiktok-web.ttwstatic.com;media-src https: blob:;worker-src blob: 'self' *.bbc.co.uk *.bbc.com;report-to worldsvc;upgrade-insecure-requests;"; - it('returns "relaxed" Csp when toggle is enabled but does not have values set', async () => { + it('returns "relaxed" Csp when toggle is enabled but does not have values set', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - await cspHeaderResponse({ + cspHeaderResponse({ ctx, service: 'pidgin', toggles: { @@ -75,10 +75,10 @@ describe('shouldServeRelaxedCsp', () => { expect(requestCsp).toEqual(expectedRelaxedCsp); }); - it('returns "relaxed" Csp when country is not in omittedCountries', async () => { + it('returns "relaxed" Csp when country is not in omittedCountries', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'ax'); - await cspHeaderResponse({ + cspHeaderResponse({ ctx, service: 'pidgin', toggles: { @@ -93,10 +93,10 @@ describe('shouldServeRelaxedCsp', () => { expect(requestCsp).toEqual(expectedRelaxedCsp); }); - it('returns "full" CSP when toggle is enabled and given country is in omittedCountries', async () => { + it('returns "full" CSP when toggle is enabled and given country is in omittedCountries', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); - await cspHeaderResponse({ + cspHeaderResponse({ ctx, service: 'pidgin', toggles: { @@ -111,10 +111,10 @@ describe('shouldServeRelaxedCsp', () => { expect(requestCsp).toEqual(expectedFullCsp); }); - it('returns "full" CSP when adsNonce.enabled is false', async () => { + it('returns "full" CSP when adsNonce.enabled is false', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); - await cspHeaderResponse({ + cspHeaderResponse({ ctx, service: 'pidgin', toggles: { adsNonce: { enabled: false, value: '' } }, diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index 21f8ba7fd3e..d99a12bdab9 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -41,7 +41,7 @@ export type CspHeaderResponseProps = { toggles: Toggles; }; -const cspHeaderResponse = async ({ +const cspHeaderResponse = ({ ctx, service, toggles, From 6494cbb08aba3f91b1daecfc16cd529279c72663 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 17:56:40 +0000 Subject: [PATCH 51/91] Update _app.page.tsx --- ws-nextjs-app/pages/_app.page.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index f7a0032b2fd..58c2cde8f9a 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -23,6 +23,8 @@ import cspHeaderResponse, { CspHeaderResponseProps, } from '#nextjs/utilities/cspHeaderResponse'; import getPathExtension from '#app/utilities/getPathExtension'; +import derivePageType from '#nextjs/utilities/derivePageType'; +import { getServerExperiments } from '#src/server/utilities/experimentHeader'; interface Props extends AppProps { pageProps: { @@ -177,6 +179,14 @@ App.getInitialProps = async ({ ctx }: AppContext) => { addServiceChainAndCspHeaders({ ctx, service, toggles }); + const pageType = derivePageType(asPath || ''); + + const serverSideExperiments = getServerExperiments({ + headers: ctx.req?.headers || {}, + service, + pageType, + }); + return { pageProps: { ...extractHeaders(req?.headers || {}), @@ -185,6 +195,7 @@ App.getInitialProps = async ({ ctx }: AppContext) => { isLite, isNextJs: true, toggles, + serverSideExperiments, }, }; }; From 74d3ad859aaaaf31d08ff84a3ce18a82ba711747 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 17:58:36 +0000 Subject: [PATCH 52/91] Add `Unknown` page type --- src/app/routes/utils/pageTypes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/routes/utils/pageTypes.ts b/src/app/routes/utils/pageTypes.ts index a2ebeb637d9..ee7fc20b4ec 100644 --- a/src/app/routes/utils/pageTypes.ts +++ b/src/app/routes/utils/pageTypes.ts @@ -19,3 +19,4 @@ export const AUDIO_PAGE = 'audio' as const; export const TV_PAGE = 'tv' as const; export const OFFLINE_PAGE = 'offline' as const; export const LIVE_TV_PAGE = 'liveTV' as const; +export const UNKNOWN_PAGE = 'Unknown' as const; From 7665b57ee7634892b21c1269f406ddbfefd03bd8 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 17:59:03 +0000 Subject: [PATCH 53/91] Remove manual `Unknown` type unions --- src/server/utilities/customMetrics/index.ts | 2 +- ws-nextjs-app/pages/_document.page.tsx | 2 +- ws-nextjs-app/utilities/derivePageType/index.ts | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server/utilities/customMetrics/index.ts b/src/server/utilities/customMetrics/index.ts index f0ef21b0dca..b6ad424233f 100644 --- a/src/server/utilities/customMetrics/index.ts +++ b/src/server/utilities/customMetrics/index.ts @@ -5,7 +5,7 @@ import onEnvironment from '../onEnvironment'; export type Params = { metricName: string; statusCode?: number | string; - pageType: PageTypes | 'Unknown'; + pageType: PageTypes; requestUrl: string; }; diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index ec46ce0fac5..f5076bf4b96 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -48,7 +48,7 @@ const handleServerLogging = ({ pageType, }: { ctx: DocumentContext; - pageType: PageTypes | 'Unknown'; + pageType: PageTypes; }) => { const url = ctx.asPath || ''; const headers = removeSensitiveHeaders(ctx.req?.headers); diff --git a/ws-nextjs-app/utilities/derivePageType/index.ts b/ws-nextjs-app/utilities/derivePageType/index.ts index d0441759b37..eb5460aceb3 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.ts @@ -12,9 +12,7 @@ import { removeRendererExtension, } from '#app/routes/utils/constructPageFetchUrl'; -export default function derivePageType( - pathname: string, -): PageTypes | 'Unknown' { +export default function derivePageType(pathname: string): PageTypes { const sanitisedPathname = new URL( removeRendererExtension(pathname), 'http://bbc.com', From 7223ac5afb57679f476453bad2654d8277d33cc2 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 18:05:59 +0000 Subject: [PATCH 54/91] Separate out header setting --- ws-nextjs-app/pages/_app.page.tsx | 41 +++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 58c2cde8f9a..a942448601c 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -24,7 +24,10 @@ import cspHeaderResponse, { } from '#nextjs/utilities/cspHeaderResponse'; import getPathExtension from '#app/utilities/getPathExtension'; import derivePageType from '#nextjs/utilities/derivePageType'; -import { getServerExperiments } from '#src/server/utilities/experimentHeader'; +import { + getExperimentVaryHeaders, + getServerExperiments, +} from '#src/server/utilities/experimentHeader'; interface Props extends AppProps { pageProps: { @@ -138,18 +141,16 @@ export default function App({ Component, pageProps }: Props) { ); } -const addServiceChainAndCspHeaders = ({ - ctx, - service, - toggles, -}: CspHeaderResponseProps) => { +const addServiceChainHeader = ({ ctx }: { ctx: NextPageContext }) => { ctx.res?.setHeader( 'req-svc-chain', addPlatformToRequestChainHeader({ headers: ctx.req?.headers as unknown as Headers, }), ); +}; +const addCspHeaders = ({ ctx, service, toggles }: CspHeaderResponseProps) => { const hostname = ctx.req?.headers.host || ''; const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; @@ -163,6 +164,27 @@ const addServiceChainAndCspHeaders = ({ } }; +const addOnionLocationHeader = ({ ctx }: { ctx: NextPageContext }) => { + const { asPath } = ctx; + + ctx.res?.setHeader( + 'onion-location', + `https://www.bbcweb3hytmzhn5d532owbu6oqadra5z3ar726vq5kgwwn6aucdccrad.onion${asPath}`, + ); +}; + +const addVaryHeaders = ( + ctx: NextPageContext, + serverSideExperiments: ServerSideExperiment[] | null, +) => { + const allVaryHeaders = ['X-Country']; + const experimentVaryHeaders = + serverSideExperiments && getExperimentVaryHeaders(serverSideExperiments); + if (experimentVaryHeaders) allVaryHeaders.push(experimentVaryHeaders); + + ctx.res?.setHeader('Vary', allVaryHeaders); +}; + // This runs on the server before rendering the page. // The props returned are passed down to ALL pages and merged with page // specific props from getInitialProps / getServerSideProps @@ -177,8 +199,6 @@ App.getInitialProps = async ({ ctx }: AppContext) => { const toggles = await getToggles(service); - addServiceChainAndCspHeaders({ ctx, service, toggles }); - const pageType = derivePageType(asPath || ''); const serverSideExperiments = getServerExperiments({ @@ -187,6 +207,11 @@ App.getInitialProps = async ({ ctx }: AppContext) => { pageType, }); + addServiceChainHeader({ ctx }); + addCspHeaders({ ctx, service, toggles }); + addOnionLocationHeader({ ctx }); + addVaryHeaders(ctx, serverSideExperiments); + return { pageProps: { ...extractHeaders(req?.headers || {}), From 1bde009ebf5f7056232a2758c948c22d36ae411e Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 18:11:53 +0000 Subject: [PATCH 55/91] Move csp header function call logic into cspHeaderResponse function --- ws-nextjs-app/pages/_app.page.tsx | 33 ++++++------------- .../utilities/cspHeaderResponse/index.ts | 12 ++++++- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index a942448601c..03b8081a2ba 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -19,9 +19,7 @@ import { UserContextProvider } from '#app/contexts/UserContext'; import extractHeaders from '#src/server/utilities/extractHeaders'; import getToggles from '#app/lib/utilities/getToggles/withCache'; import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; -import cspHeaderResponse, { - CspHeaderResponseProps, -} from '#nextjs/utilities/cspHeaderResponse'; +import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; import getPathExtension from '#app/utilities/getPathExtension'; import derivePageType from '#nextjs/utilities/derivePageType'; import { @@ -150,20 +148,6 @@ const addServiceChainHeader = ({ ctx }: { ctx: NextPageContext }) => { ); }; -const addCspHeaders = ({ ctx, service, toggles }: CspHeaderResponseProps) => { - const hostname = ctx.req?.headers.host || ''; - - const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; - - const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); - - const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; - - if (PRODUCTION_ONLY) { - cspHeaderResponse({ ctx, service, toggles }); - } -}; - const addOnionLocationHeader = ({ ctx }: { ctx: NextPageContext }) => { const { asPath } = ctx; @@ -173,10 +157,13 @@ const addOnionLocationHeader = ({ ctx }: { ctx: NextPageContext }) => { ); }; -const addVaryHeaders = ( - ctx: NextPageContext, - serverSideExperiments: ServerSideExperiment[] | null, -) => { +const addVaryHeaders = ({ + ctx, + serverSideExperiments, +}: { + ctx: NextPageContext; + serverSideExperiments: ServerSideExperiment[] | null; +}) => { const allVaryHeaders = ['X-Country']; const experimentVaryHeaders = serverSideExperiments && getExperimentVaryHeaders(serverSideExperiments); @@ -208,9 +195,9 @@ App.getInitialProps = async ({ ctx }: AppContext) => { }); addServiceChainHeader({ ctx }); - addCspHeaders({ ctx, service, toggles }); + cspHeaderResponse({ ctx, service, toggles }); addOnionLocationHeader({ ctx }); - addVaryHeaders(ctx, serverSideExperiments); + addVaryHeaders({ ctx, serverSideExperiments }); return { pageProps: { diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts index d99a12bdab9..29d259c0359 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.ts @@ -35,7 +35,7 @@ const isRelaxedCspEnabled = ( return !omittedCountriesList.includes(country.toLowerCase()); }; -export type CspHeaderResponseProps = { +type CspHeaderResponseProps = { ctx: NextPageContext; service: Services; toggles: Toggles; @@ -46,6 +46,16 @@ const cspHeaderResponse = ({ service, toggles, }: CspHeaderResponseProps) => { + const hostname = ctx.req?.headers.host || ''; + + const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; + + const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); + + const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; + + if (!PRODUCTION_ONLY) return; + const reqUrl = ctx.req?.url || ''; const { isAmp } = getPathExtension(reqUrl); const isLive = isLiveEnv(); From bb24194d33b4cce4cb03873afbc6ace622b46cfd Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 18:13:36 +0000 Subject: [PATCH 56/91] Fix tests --- .../utilities/cspHeaderResponse/index.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts index 72100ff88ca..4fc6a57ac1d 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts @@ -33,6 +33,12 @@ const policies = [ ]; describe('cspHeaderResponse', () => { + const processEnv = process.env; + + beforeEach(() => { + process.env = { ...processEnv, NODE_ENV: 'production' }; + }); + it.each(policies)('should set %s in the request CSP', policy => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); @@ -44,9 +50,31 @@ describe('cspHeaderResponse', () => { expect((requestCsp as string).includes(policy)).toBe(true); }); + + it('should not set CSP headers in non-production environments', () => { + const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); + + process.env = { ...processEnv, NODE_ENV: 'development' }; + + cspHeaderResponse({ ctx, service: 'pidgin', toggles: {} }); + + const setHeaderCalls = (ctx.res?.setHeader as jest.Mock).mock.calls; + + const cspHeaderCall = setHeaderCalls.find( + call => call[0] === 'Content-Security-Policy', + ); + + expect(cspHeaderCall).toBeUndefined(); + }); }); describe('shouldServeRelaxedCsp', () => { + const processEnv = process.env; + + beforeEach(() => { + process.env = { ...processEnv, NODE_ENV: 'production' }; + }); + afterEach(() => { jest.clearAllMocks(); }); From d788d2623bde4b2d76516b4033ef0e3b77cf5dc4 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 18:14:44 +0000 Subject: [PATCH 57/91] Rename folder --- ws-nextjs-app/pages/_app.page.tsx | 4 ++-- .../index.test.ts | 16 ++++++++-------- .../{cspHeaderResponse => addCspHeader}/index.ts | 10 +++------- 3 files changed, 13 insertions(+), 17 deletions(-) rename ws-nextjs-app/utilities/{cspHeaderResponse => addCspHeader}/index.test.ts (96%) rename ws-nextjs-app/utilities/{cspHeaderResponse => addCspHeader}/index.ts (94%) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 03b8081a2ba..6fafcb76cea 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -19,7 +19,7 @@ import { UserContextProvider } from '#app/contexts/UserContext'; import extractHeaders from '#src/server/utilities/extractHeaders'; import getToggles from '#app/lib/utilities/getToggles/withCache'; import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; -import cspHeaderResponse from '#nextjs/utilities/cspHeaderResponse'; +import addCspHeader from '#nextjs/utilities/addCspHeader'; import getPathExtension from '#app/utilities/getPathExtension'; import derivePageType from '#nextjs/utilities/derivePageType'; import { @@ -195,7 +195,7 @@ App.getInitialProps = async ({ ctx }: AppContext) => { }); addServiceChainHeader({ ctx }); - cspHeaderResponse({ ctx, service, toggles }); + addCspHeader({ ctx, service, toggles }); addOnionLocationHeader({ ctx }); addVaryHeaders({ ctx, serverSideExperiments }); diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts b/ws-nextjs-app/utilities/addCspHeader/index.test.ts similarity index 96% rename from ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts rename to ws-nextjs-app/utilities/addCspHeader/index.test.ts index 4fc6a57ac1d..f9c00eed379 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.test.ts +++ b/ws-nextjs-app/utilities/addCspHeader/index.test.ts @@ -1,5 +1,5 @@ import { NextPageContext } from 'next/types'; -import cspHeaderResponse from '.'; +import addCspHeader from '.'; const createDocumentContext = (pathname: string, country?: string) => { const url = new URL(`https://www.test.bbc.com${pathname}`); @@ -32,7 +32,7 @@ const policies = [ 'upgrade-insecure-requests', ]; -describe('cspHeaderResponse', () => { +describe('addCspHeader', () => { const processEnv = process.env; beforeEach(() => { @@ -42,7 +42,7 @@ describe('cspHeaderResponse', () => { it.each(policies)('should set %s in the request CSP', policy => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - cspHeaderResponse({ ctx, service: 'pidgin', toggles: {} }); + addCspHeader({ ctx, service: 'pidgin', toggles: {} }); const requestCsp = (ctx.res?.setHeader as jest.Mock).mock.calls.find( call => call[0] === 'Content-Security-Policy', @@ -56,7 +56,7 @@ describe('cspHeaderResponse', () => { process.env = { ...processEnv, NODE_ENV: 'development' }; - cspHeaderResponse({ ctx, service: 'pidgin', toggles: {} }); + addCspHeader({ ctx, service: 'pidgin', toggles: {} }); const setHeaderCalls = (ctx.res?.setHeader as jest.Mock).mock.calls; @@ -88,7 +88,7 @@ describe('shouldServeRelaxedCsp', () => { it('returns "relaxed" Csp when toggle is enabled but does not have values set', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt'); - cspHeaderResponse({ + addCspHeader({ ctx, service: 'pidgin', toggles: { @@ -106,7 +106,7 @@ describe('shouldServeRelaxedCsp', () => { it('returns "relaxed" Csp when country is not in omittedCountries', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'ax'); - cspHeaderResponse({ + addCspHeader({ ctx, service: 'pidgin', toggles: { @@ -124,7 +124,7 @@ describe('shouldServeRelaxedCsp', () => { it('returns "full" CSP when toggle is enabled and given country is in omittedCountries', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); - cspHeaderResponse({ + addCspHeader({ ctx, service: 'pidgin', toggles: { @@ -142,7 +142,7 @@ describe('shouldServeRelaxedCsp', () => { it('returns "full" CSP when adsNonce.enabled is false', () => { const ctx = createDocumentContext('/pidgin/live/c7p765ynk9qt', 'gb'); - cspHeaderResponse({ + addCspHeader({ ctx, service: 'pidgin', toggles: { adsNonce: { enabled: false, value: '' } }, diff --git a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts b/ws-nextjs-app/utilities/addCspHeader/index.ts similarity index 94% rename from ws-nextjs-app/utilities/cspHeaderResponse/index.ts rename to ws-nextjs-app/utilities/addCspHeader/index.ts index 29d259c0359..f7724d8eef8 100644 --- a/ws-nextjs-app/utilities/cspHeaderResponse/index.ts +++ b/ws-nextjs-app/utilities/addCspHeader/index.ts @@ -35,17 +35,13 @@ const isRelaxedCspEnabled = ( return !omittedCountriesList.includes(country.toLowerCase()); }; -type CspHeaderResponseProps = { +type AddCspHeaderProps = { ctx: NextPageContext; service: Services; toggles: Toggles; }; -const cspHeaderResponse = ({ - ctx, - service, - toggles, -}: CspHeaderResponseProps) => { +const addCspHeader = ({ ctx, service, toggles }: AddCspHeaderProps) => { const hostname = ctx.req?.headers.host || ''; const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; @@ -115,4 +111,4 @@ const cspHeaderResponse = ({ ); }; -export default cspHeaderResponse; +export default addCspHeader; From 68e731c9195c223fc373056f36cf530e17d18d92 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 18:17:29 +0000 Subject: [PATCH 58/91] Move into separate utils folder --- ws-nextjs-app/pages/_app.page.tsx | 42 ++----------------- .../utilities/addOnionLocationHeader/index.ts | 12 ++++++ .../utilities/addServiceChainHeader/index.ts | 13 ++++++ .../utilities/addVaryHeader/index.ts | 20 +++++++++ 4 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 ws-nextjs-app/utilities/addOnionLocationHeader/index.ts create mode 100644 ws-nextjs-app/utilities/addServiceChainHeader/index.ts create mode 100644 ws-nextjs-app/utilities/addVaryHeader/index.ts diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 6fafcb76cea..532d51e2869 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -18,14 +18,13 @@ import { EventTrackingContextProvider } from '#app/contexts/EventTrackingContext import { UserContextProvider } from '#app/contexts/UserContext'; import extractHeaders from '#src/server/utilities/extractHeaders'; import getToggles from '#app/lib/utilities/getToggles/withCache'; -import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; import addCspHeader from '#nextjs/utilities/addCspHeader'; import getPathExtension from '#app/utilities/getPathExtension'; import derivePageType from '#nextjs/utilities/derivePageType'; -import { - getExperimentVaryHeaders, - getServerExperiments, -} from '#src/server/utilities/experimentHeader'; +import { getServerExperiments } from '#src/server/utilities/experimentHeader'; +import addServiceChainHeader from '#nextjs/utilities/addServiceChainHeader'; +import addOnionLocationHeader from '#nextjs/utilities/addOnionLocationHeader'; +import addVaryHeaders from '#nextjs/utilities/addVaryHeader'; interface Props extends AppProps { pageProps: { @@ -139,39 +138,6 @@ export default function App({ Component, pageProps }: Props) { ); } -const addServiceChainHeader = ({ ctx }: { ctx: NextPageContext }) => { - ctx.res?.setHeader( - 'req-svc-chain', - addPlatformToRequestChainHeader({ - headers: ctx.req?.headers as unknown as Headers, - }), - ); -}; - -const addOnionLocationHeader = ({ ctx }: { ctx: NextPageContext }) => { - const { asPath } = ctx; - - ctx.res?.setHeader( - 'onion-location', - `https://www.bbcweb3hytmzhn5d532owbu6oqadra5z3ar726vq5kgwwn6aucdccrad.onion${asPath}`, - ); -}; - -const addVaryHeaders = ({ - ctx, - serverSideExperiments, -}: { - ctx: NextPageContext; - serverSideExperiments: ServerSideExperiment[] | null; -}) => { - const allVaryHeaders = ['X-Country']; - const experimentVaryHeaders = - serverSideExperiments && getExperimentVaryHeaders(serverSideExperiments); - if (experimentVaryHeaders) allVaryHeaders.push(experimentVaryHeaders); - - ctx.res?.setHeader('Vary', allVaryHeaders); -}; - // This runs on the server before rendering the page. // The props returned are passed down to ALL pages and merged with page // specific props from getInitialProps / getServerSideProps diff --git a/ws-nextjs-app/utilities/addOnionLocationHeader/index.ts b/ws-nextjs-app/utilities/addOnionLocationHeader/index.ts new file mode 100644 index 00000000000..8e494f458d4 --- /dev/null +++ b/ws-nextjs-app/utilities/addOnionLocationHeader/index.ts @@ -0,0 +1,12 @@ +import { NextPageContext } from 'next/types'; + +const addOnionLocationHeader = ({ ctx }: { ctx: NextPageContext }) => { + const { asPath } = ctx; + + ctx.res?.setHeader( + 'onion-location', + `https://www.bbcweb3hytmzhn5d532owbu6oqadra5z3ar726vq5kgwwn6aucdccrad.onion${asPath}`, + ); +}; + +export default addOnionLocationHeader; diff --git a/ws-nextjs-app/utilities/addServiceChainHeader/index.ts b/ws-nextjs-app/utilities/addServiceChainHeader/index.ts new file mode 100644 index 00000000000..9d78b3aab67 --- /dev/null +++ b/ws-nextjs-app/utilities/addServiceChainHeader/index.ts @@ -0,0 +1,13 @@ +import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; +import { NextPageContext } from 'next/types'; + +const addServiceChainHeader = ({ ctx }: { ctx: NextPageContext }) => { + ctx.res?.setHeader( + 'req-svc-chain', + addPlatformToRequestChainHeader({ + headers: ctx.req?.headers as unknown as Headers, + }), + ); +}; + +export default addServiceChainHeader; diff --git a/ws-nextjs-app/utilities/addVaryHeader/index.ts b/ws-nextjs-app/utilities/addVaryHeader/index.ts new file mode 100644 index 00000000000..aefc634fd00 --- /dev/null +++ b/ws-nextjs-app/utilities/addVaryHeader/index.ts @@ -0,0 +1,20 @@ +import { ServerSideExperiment } from '#app/models/types/global'; +import { getExperimentVaryHeaders } from '#src/server/utilities/experimentHeader'; +import { NextPageContext } from 'next/types'; + +const addVaryHeaders = ({ + ctx, + serverSideExperiments, +}: { + ctx: NextPageContext; + serverSideExperiments: ServerSideExperiment[] | null; +}) => { + const allVaryHeaders = ['X-Country']; + const experimentVaryHeaders = + serverSideExperiments && getExperimentVaryHeaders(serverSideExperiments); + if (experimentVaryHeaders) allVaryHeaders.push(experimentVaryHeaders); + + ctx.res?.setHeader('Vary', allVaryHeaders); +}; + +export default addVaryHeaders; From b454b9c6d4ea0243c86016ab5349e82121f9ca6a Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 18:19:51 +0000 Subject: [PATCH 59/91] Remove server experiments logic from articles route --- .../pages/[service]/articles/handleArticleRoute.ts | 8 -------- ws-nextjs-app/utilities/addCspHeader/index.ts | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts index aebcdbff7de..7f6be37362d 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts @@ -10,7 +10,6 @@ import handleError from '#app/routes/utils/handleError'; import { PageTypes } from '#app/models/types/global'; import { ArticleMetadata } from '#app/models/types/optimo'; -import { getServerExperiments } from '#server/utilities/experimentHeader'; import augmentWithDisclaimer from './augmentWithDisclaimer'; import shouldRender from './shouldRender'; import getPageData from '../../../utilities/pageRequests/getPageData'; @@ -176,12 +175,6 @@ export default async (context: GetServerSidePropsContext) => { const derivedPageType = getDerivedArticleType(article.metadata); - const serverSideExperiments = getServerExperiments({ - headers: reqHeaders, - service, - pageType: derivedPageType, - }); - return { props: { country, @@ -201,7 +194,6 @@ export default async (context: GetServerSidePropsContext) => { }, pageType: derivedPageType, pathname: resolvedUrlWithoutQuery, - serverSideExperiments, service, status, variant: variant || null, diff --git a/ws-nextjs-app/utilities/addCspHeader/index.ts b/ws-nextjs-app/utilities/addCspHeader/index.ts index f7724d8eef8..b211300bf36 100644 --- a/ws-nextjs-app/utilities/addCspHeader/index.ts +++ b/ws-nextjs-app/utilities/addCspHeader/index.ts @@ -5,6 +5,8 @@ import isLiveEnv from '#lib/utilities/isLive'; import { Services, Toggles } from '#app/models/types/global'; import SERVICES from '#app/lib/config/services'; +const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; + const directiveToString = (directives: Record) => { const map = new Map(Object.entries(directives)); let cspValue = ''; @@ -44,8 +46,6 @@ type AddCspHeaderProps = { const addCspHeader = ({ ctx, service, toggles }: AddCspHeaderProps) => { const hostname = ctx.req?.headers.host || ''; - const LOCALHOST_DOMAINS = ['localhost', '127.0.0.1']; - const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; From c79d1c715fa37c6eb5a4b33110b842a20f82a7c9 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Fri, 12 Dec 2025 18:34:30 +0000 Subject: [PATCH 60/91] add tests --- .../addOnionLocationHeader/index.test.ts | 25 ++++++++++++ .../addServiceChainHeader/index.test.ts | 29 ++++++++++++++ .../utilities/addVaryHeader/index.test.ts | 38 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 ws-nextjs-app/utilities/addOnionLocationHeader/index.test.ts create mode 100644 ws-nextjs-app/utilities/addServiceChainHeader/index.test.ts create mode 100644 ws-nextjs-app/utilities/addVaryHeader/index.test.ts diff --git a/ws-nextjs-app/utilities/addOnionLocationHeader/index.test.ts b/ws-nextjs-app/utilities/addOnionLocationHeader/index.test.ts new file mode 100644 index 00000000000..036ed74e108 --- /dev/null +++ b/ws-nextjs-app/utilities/addOnionLocationHeader/index.test.ts @@ -0,0 +1,25 @@ +import { NextPageContext } from 'next/types'; +import addOnionLocationHeader from '.'; + +describe('addOnionLocationHeader', () => { + const mockSetHeader = jest.fn(); + const mockCtx = { + asPath: '/test-path', + res: { + setHeader: mockSetHeader, + }, + } as unknown as NextPageContext; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should add the correct onion-location header', () => { + addOnionLocationHeader({ ctx: mockCtx }); + + expect(mockSetHeader).toHaveBeenCalledWith( + 'onion-location', + 'https://www.bbcweb3hytmzhn5d532owbu6oqadra5z3ar726vq5kgwwn6aucdccrad.onion/test-path', + ); + }); +}); diff --git a/ws-nextjs-app/utilities/addServiceChainHeader/index.test.ts b/ws-nextjs-app/utilities/addServiceChainHeader/index.test.ts new file mode 100644 index 00000000000..f069e8a10e9 --- /dev/null +++ b/ws-nextjs-app/utilities/addServiceChainHeader/index.test.ts @@ -0,0 +1,29 @@ +import { NextPageContext } from 'next/types'; +import addServiceChainHeader from '.'; + +describe('addServiceChainHeader', () => { + const mockSetHeader = jest.fn(); + const mockCtx = { + req: { + headers: { + 'req-svc-chain': 'UPSTREAM_A', + }, + }, + res: { + setHeader: mockSetHeader, + }, + } as unknown as NextPageContext; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should add the correct req-svc-chain header', () => { + addServiceChainHeader({ ctx: mockCtx }); + + expect(mockSetHeader).toHaveBeenCalledWith( + 'req-svc-chain', + expect.stringContaining('UPSTREAM_A'), + ); + }); +}); diff --git a/ws-nextjs-app/utilities/addVaryHeader/index.test.ts b/ws-nextjs-app/utilities/addVaryHeader/index.test.ts new file mode 100644 index 00000000000..9745fe36ca0 --- /dev/null +++ b/ws-nextjs-app/utilities/addVaryHeader/index.test.ts @@ -0,0 +1,38 @@ +import { NextPageContext } from 'next/types'; +import addVaryHeaders from '.'; + +describe('addVaryHeaders', () => { + const mockSetHeader = jest.fn(); + const mockCtx = { + res: { + setHeader: mockSetHeader, + }, + } as unknown as NextPageContext; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should add the correct Vary headers', () => { + addVaryHeaders({ ctx: mockCtx, serverSideExperiments: [] }); + + expect(mockSetHeader).toHaveBeenCalledWith('Vary', ['X-Country']); + }); + + it('should include server-side experiment headers in Vary', () => { + const serverSideExperiments = [ + { + experimentName: 'experiment-1', + variation: 'A', + enabled: true, + }, + ]; + + addVaryHeaders({ ctx: mockCtx, serverSideExperiments }); + + expect(mockSetHeader).toHaveBeenCalledWith('Vary', [ + 'X-Country', + 'mvt-experiment-1', + ]); + }); +}); From 0df6419b464caaf0ff316df4666ef64bcd286f2d Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sat, 13 Dec 2025 12:19:27 +0000 Subject: [PATCH 61/91] Convert _app to class component to be consistent with _document --- ws-nextjs-app/pages/_app.page.tsx | 222 +++++++++++++++--------------- 1 file changed, 113 insertions(+), 109 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 532d51e2869..a5bfa86f7b6 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -1,4 +1,4 @@ -import type { AppContext, AppProps } from 'next/app'; +import App, { AppContext, AppProps } from 'next/app'; import { NextPageContext } from 'next/types'; import { ATIData } from '#app/components/ATIAnalytics/types'; import ThemeProvider from '#app/components/ThemeProvider'; @@ -57,123 +57,127 @@ interface Props extends AppProps { }; } -export default function App({ Component, pageProps }: Props) { - const { - bbcOrigin, - id, - isAmp, - isApp = false, - isLite = false, - isNextJs = true, - isAvEmbeds = false, - serverSideExperiments = null, - pageData, - pageLang = '', - pageType, - pathname, - service, - showAdsBasedOnLocation, - showCookieBannerBasedOnCountry = true, - status, - timeOnServer, - toggles, - variant, - isUK, - country, - } = pageProps; +export default class CustomApp extends App { + // This runs on the server before rendering the page. + // The props returned are passed down to ALL pages and merged with page + // specific props from their getInitialProps / getServerSideProps functions + static async getInitialProps({ ctx }: AppContext) { + const { req, asPath } = ctx as NextPageContext; - const { metadata: { atiAnalytics = undefined } = {} } = pageData ?? {}; + const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); - const RenderChildrenOrError = - status === 200 ? ( - - ) : ( - - ); + const routeSegments = asPath?.split('/')?.filter(Boolean); - return ( - - - - - {isAvEmbeds ? ( - - {RenderChildrenOrError} - - ) : ( - - - - {RenderChildrenOrError} - - - - )} - - - - - ); -} - -// This runs on the server before rendering the page. -// The props returned are passed down to ALL pages and merged with page -// specific props from getInitialProps / getServerSideProps -App.getInitialProps = async ({ ctx }: AppContext) => { - const { req, asPath } = ctx as NextPageContext; - - const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); + const [service] = (routeSegments || []) as [Services]; - const routeSegments = asPath?.split('/')?.filter(Boolean); + const toggles = await getToggles(service); - const [service] = (routeSegments || []) as [Services]; + const pageType = derivePageType(asPath || ''); - const toggles = await getToggles(service); + const serverSideExperiments = getServerExperiments({ + headers: ctx.req?.headers || {}, + service, + pageType, + }); - const pageType = derivePageType(asPath || ''); + addServiceChainHeader({ ctx }); + addCspHeader({ ctx, service, toggles }); + addOnionLocationHeader({ ctx }); + addVaryHeaders({ ctx, serverSideExperiments }); - const serverSideExperiments = getServerExperiments({ - headers: ctx.req?.headers || {}, - service, - pageType, - }); + return { + pageProps: { + ...extractHeaders(req?.headers || {}), + isApp, + isAmp, + isLite, + isNextJs: true, + toggles, + serverSideExperiments, + }, + }; + } - addServiceChainHeader({ ctx }); - addCspHeader({ ctx, service, toggles }); - addOnionLocationHeader({ ctx }); - addVaryHeaders({ ctx, serverSideExperiments }); + render() { + const { Component, pageProps } = this.props; - return { - pageProps: { - ...extractHeaders(req?.headers || {}), - isApp, + const { + bbcOrigin, + id, isAmp, - isLite, - isNextJs: true, + isApp = false, + isLite = false, + isNextJs = true, + isAvEmbeds = false, + serverSideExperiments = null, + pageData, + pageLang = '', + pageType, + pathname, + service, + showAdsBasedOnLocation, + showCookieBannerBasedOnCountry = true, + status, + timeOnServer, toggles, - serverSideExperiments, - }, - }; -}; + variant, + isUK, + country, + } = pageProps; + + const { metadata: { atiAnalytics = undefined } = {} } = pageData ?? {}; + + const RenderChildrenOrError = + status === 200 ? ( + + ) : ( + + ); + + return ( + + + + + {isAvEmbeds ? ( + + {RenderChildrenOrError} + + ) : ( + + + + {RenderChildrenOrError} + + + + )} + + + + + ); + } +} From 0bf2aeb5d00c9f6f6967c19b2b48da17c5e2ddd8 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sat, 13 Dec 2025 12:27:32 +0000 Subject: [PATCH 62/91] Remove unneeded type cast --- ws-nextjs-app/pages/_app.page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index a5bfa86f7b6..d9ce588b6c0 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -1,5 +1,4 @@ import App, { AppContext, AppProps } from 'next/app'; -import { NextPageContext } from 'next/types'; import { ATIData } from '#app/components/ATIAnalytics/types'; import ThemeProvider from '#app/components/ThemeProvider'; import { ToggleContextProvider } from '#app/contexts/ToggleContext'; @@ -62,7 +61,7 @@ export default class CustomApp extends App { // The props returned are passed down to ALL pages and merged with page // specific props from their getInitialProps / getServerSideProps functions static async getInitialProps({ ctx }: AppContext) { - const { req, asPath } = ctx as NextPageContext; + const { req, asPath } = ctx; const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); From aff87f0f0b139b90fac0499bbd786c5be160d7d6 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sat, 13 Dec 2025 12:31:45 +0000 Subject: [PATCH 63/91] Move `handleServerLogging` to separate file --- ws-nextjs-app/pages/_document.page.tsx | 53 +----------------- .../utilities/handleServerLogging/index.ts | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 ws-nextjs-app/utilities/handleServerLogging/index.ts diff --git a/ws-nextjs-app/pages/_document.page.tsx b/ws-nextjs-app/pages/_document.page.tsx index f5076bf4b96..a685b6b2196 100644 --- a/ws-nextjs-app/pages/_document.page.tsx +++ b/ws-nextjs-app/pages/_document.page.tsx @@ -22,67 +22,16 @@ import { import AmpRenderer from '#server/Document/Renderers/AmpRenderer'; import LiteRenderer from '#server/Document/Renderers/LiteRenderer'; import litePageTransforms from '#server/Document/Renderers/litePageTransforms'; -import sendCustomMetric from '#server/utilities/customMetrics'; -import { NON_200_RESPONSE } from '#server/utilities/customMetrics/metrics.const'; -import nodeLogger from '#lib/logger.node'; -import { - SERVER_SIDE_RENDER_REQUEST_RECEIVED, - SERVER_SIDE_REQUEST_FAILED, -} from '#lib/logger.const'; -import { OK, INTERNAL_SERVER_ERROR } from '#app/lib/statusCodes.const'; import NO_JS_CLASSNAME from '#app/lib/noJs.const'; import getPathExtension from '#app/utilities/getPathExtension'; import ReverbTemplate from '#src/server/Document/Renderers/ReverbTemplate'; -import { PageTypes } from '#app/models/types/global'; import ComponentTracking from '#src/server/Document/Renderers/ComponentTracking'; import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript'; -import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders'; +import handleServerLogging from '#nextjs/utilities/handleServerLogging'; import derivePageType from '../utilities/derivePageType'; -const logger = nodeLogger(__filename); - -const handleServerLogging = ({ - ctx, - pageType, -}: { - ctx: DocumentContext; - pageType: PageTypes; -}) => { - const url = ctx.asPath || ''; - const headers = removeSensitiveHeaders(ctx.req?.headers); - const { statusCode } = ctx.res || {}; - const { cause, message, name, stack } = ctx.err || {}; - - switch (statusCode) { - case OK: - logger.debug(SERVER_SIDE_RENDER_REQUEST_RECEIVED, { - url, - headers, - pageType, - }); - break; - case INTERNAL_SERVER_ERROR: - sendCustomMetric({ - metricName: NON_200_RESPONSE, - statusCode, - pageType, - requestUrl: url, - }); - logger.error(SERVER_SIDE_REQUEST_FAILED, { - status: INTERNAL_SERVER_ERROR, - message: { cause, message, name, stack, url }, - url, - headers, - pageType, - }); - break; - default: - break; - } -}; - type DocProps = { clientSideEnvVariables: EnvConfig; css: string; diff --git a/ws-nextjs-app/utilities/handleServerLogging/index.ts b/ws-nextjs-app/utilities/handleServerLogging/index.ts new file mode 100644 index 00000000000..e88fd9b4ef3 --- /dev/null +++ b/ws-nextjs-app/utilities/handleServerLogging/index.ts @@ -0,0 +1,56 @@ +import { + SERVER_SIDE_RENDER_REQUEST_RECEIVED, + SERVER_SIDE_REQUEST_FAILED, +} from '#app/lib/logger.const'; +import { INTERNAL_SERVER_ERROR, OK } from '#app/lib/statusCodes.const'; +import nodeLogger from '#lib/logger.node'; +import { DocumentContext } from 'next/document'; + +import { PageTypes } from '#app/models/types/global'; +import sendCustomMetric from '#src/server/utilities/customMetrics'; +import { NON_200_RESPONSE } from '#src/server/utilities/customMetrics/metrics.const'; +import removeSensitiveHeaders from '../removeSensitiveHeaders'; + +const logger = nodeLogger(__filename); + +const handleServerLogging = ({ + ctx, + pageType, +}: { + ctx: DocumentContext; + pageType: PageTypes; +}) => { + const url = ctx.asPath || ''; + const headers = removeSensitiveHeaders(ctx.req?.headers); + const { statusCode } = ctx.res || {}; + const { cause, message, name, stack } = ctx.err || {}; + + switch (statusCode) { + case OK: + logger.debug(SERVER_SIDE_RENDER_REQUEST_RECEIVED, { + url, + headers, + pageType, + }); + break; + case INTERNAL_SERVER_ERROR: + sendCustomMetric({ + metricName: NON_200_RESPONSE, + statusCode, + pageType, + requestUrl: url, + }); + logger.error(SERVER_SIDE_REQUEST_FAILED, { + status: INTERNAL_SERVER_ERROR, + message: { cause, message, name, stack, url }, + url, + headers, + pageType, + }); + break; + default: + break; + } +}; + +export default handleServerLogging; From 7217d9233e34f8c767b547fb0079bfd98151c985 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sat, 13 Dec 2025 14:31:41 +0000 Subject: [PATCH 64/91] Update _app.page.tsx --- ws-nextjs-app/pages/_app.page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index d9ce588b6c0..3bb7b3eb99d 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -16,11 +16,11 @@ import { RequestContextProvider } from '#app/contexts/RequestContext'; import { EventTrackingContextProvider } from '#app/contexts/EventTrackingContext'; import { UserContextProvider } from '#app/contexts/UserContext'; import extractHeaders from '#src/server/utilities/extractHeaders'; +import { getServerExperiments } from '#src/server/utilities/experimentHeader'; import getToggles from '#app/lib/utilities/getToggles/withCache'; -import addCspHeader from '#nextjs/utilities/addCspHeader'; import getPathExtension from '#app/utilities/getPathExtension'; +import addCspHeader from '#nextjs/utilities/addCspHeader'; import derivePageType from '#nextjs/utilities/derivePageType'; -import { getServerExperiments } from '#src/server/utilities/experimentHeader'; import addServiceChainHeader from '#nextjs/utilities/addServiceChainHeader'; import addOnionLocationHeader from '#nextjs/utilities/addOnionLocationHeader'; import addVaryHeaders from '#nextjs/utilities/addVaryHeader'; @@ -61,7 +61,7 @@ export default class CustomApp extends App { // The props returned are passed down to ALL pages and merged with page // specific props from their getInitialProps / getServerSideProps functions static async getInitialProps({ ctx }: AppContext) { - const { req, asPath } = ctx; + const { asPath } = ctx; const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); @@ -86,7 +86,7 @@ export default class CustomApp extends App { return { pageProps: { - ...extractHeaders(req?.headers || {}), + ...extractHeaders(ctx.req?.headers || {}), isApp, isAmp, isLite, From 4ec7c9ba3e20c1ff718adf037b1dfcac32f4fc4b Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sun, 14 Dec 2025 18:05:27 +0000 Subject: [PATCH 65/91] Update _app.page.tsx --- ws-nextjs-app/pages/_app.page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 3bb7b3eb99d..4ec2095d53c 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -57,7 +57,6 @@ interface Props extends AppProps { } export default class CustomApp extends App { - // This runs on the server before rendering the page. // The props returned are passed down to ALL pages and merged with page // specific props from their getInitialProps / getServerSideProps functions static async getInitialProps({ ctx }: AppContext) { From bb3688ac20de4068e4450be48d5db8477a709753 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sun, 14 Dec 2025 18:08:54 +0000 Subject: [PATCH 66/91] Update _app.page.tsx --- ws-nextjs-app/pages/_app.page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 4ec2095d53c..08275fb87ab 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -90,8 +90,8 @@ export default class CustomApp extends App { isAmp, isLite, isNextJs: true, - toggles, serverSideExperiments, + toggles, }, }; } From a098527aa6148824ca72964328315becdc2f9e7a Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sun, 14 Dec 2025 18:09:51 +0000 Subject: [PATCH 67/91] Update _app.page.tsx --- ws-nextjs-app/pages/_app.page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 08275fb87ab..f2fad5ebbb0 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -57,8 +57,8 @@ interface Props extends AppProps { } export default class CustomApp extends App { - // The props returned are passed down to ALL pages and merged with page - // specific props from their getInitialProps / getServerSideProps functions + // The 'pageProps' returned are passed down to ALL pages and merged with page + // specific 'pageProps' from their getInitialProps / getServerSideProps functions static async getInitialProps({ ctx }: AppContext) { const { asPath } = ctx; From f0d4683bf933745b6822a3869aa683fd480312a9 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sun, 14 Dec 2025 22:28:20 +0000 Subject: [PATCH 68/91] Add page type tests to `derivePageType` --- .../utilities/derivePageType/index.test.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/ws-nextjs-app/utilities/derivePageType/index.test.ts b/ws-nextjs-app/utilities/derivePageType/index.test.ts index 6259b26a06b..4671bff7517 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.test.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.test.ts @@ -1,17 +1,47 @@ -import { LIVE_PAGE, UGC_PAGE } from '#app/routes/utils/pageTypes'; +import { + LIVE_PAGE, + UGC_PAGE, + AV_EMBEDS, + DOWNLOADS_PAGE, + ARTICLE_PAGE, +} from '#app/routes/utils/pageTypes'; import derivePageType from '.'; describe('derivePageType', () => { + it("should return LIVE_PAGE if pathname includes 'live'", () => { + const pathname = '/burmese/live/xxxxxxxxx'; + const result = derivePageType(pathname); + expect(result).toEqual(LIVE_PAGE); + }); + it("should return UGC_PAGE if pathname includes 'send'", () => { const pathname = '/burmese/send/xxxxxxxxx'; const result = derivePageType(pathname); expect(result).toEqual(UGC_PAGE); }); - it("should return LIVE_PAGE if pathname includes 'live'", () => { - const pathname = '/burmese/live/xxxxxxxxx'; + it("should return AV_EMBEDS if pathname includes 'av-embeds'", () => { + const pathname = '/burmese/av-embeds/xxxxxxxxx'; const result = derivePageType(pathname); - expect(result).toEqual(LIVE_PAGE); + expect(result).toEqual(AV_EMBEDS); + }); + + it("should return DOWNLOADS_PAGE if pathname includes 'downloads'", () => { + const pathname = '/burmese/downloads/xxxxxxxxx'; + const result = derivePageType(pathname); + expect(result).toEqual(DOWNLOADS_PAGE); + }); + + it('should return ARTICLE_PAGE if pathname matches Optimo ID pattern', () => { + const pathname = '/burmese/articles/c0000000000o'; + const result = derivePageType(pathname); + expect(result).toEqual(ARTICLE_PAGE); + }); + + it('should return ARTICLE_PAGE if pathname matches CPS ID pattern', () => { + const pathname = '/burmese/instituional-1234567'; + const result = derivePageType(pathname); + expect(result).toEqual(ARTICLE_PAGE); }); it('should return Unknown if pathname does not include live or send', () => { From 24dfb6061c3c6e5fa6500456544ae8238853ed79 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Sun, 14 Dec 2025 22:30:05 +0000 Subject: [PATCH 69/91] Use `page-type` header first if available --- ws-nextjs-app/pages/_app.page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index f2fad5ebbb0..e1319ef34ca 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -70,7 +70,9 @@ export default class CustomApp extends App { const toggles = await getToggles(service); - const pageType = derivePageType(asPath || ''); + const pageType = + (ctx.req?.headers['page-type'] as PageTypes) || + derivePageType(asPath || ''); const serverSideExperiments = getServerExperiments({ headers: ctx.req?.headers || {}, From 42c47bd7f79604e0a60d61c1e3dd49aba0b52739 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 16 Dec 2025 20:35:59 +0000 Subject: [PATCH 70/91] Fix import order --- ws-nextjs-app/utilities/addServiceChainHeader/index.ts | 2 +- ws-nextjs-app/utilities/addVaryHeader/index.ts | 2 +- ws-nextjs-app/utilities/handleServerLogging/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ws-nextjs-app/utilities/addServiceChainHeader/index.ts b/ws-nextjs-app/utilities/addServiceChainHeader/index.ts index 9d78b3aab67..fdb1f2f3632 100644 --- a/ws-nextjs-app/utilities/addServiceChainHeader/index.ts +++ b/ws-nextjs-app/utilities/addServiceChainHeader/index.ts @@ -1,5 +1,5 @@ -import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; import { NextPageContext } from 'next/types'; +import addPlatformToRequestChainHeader from '#src/server/utilities/addPlatformToRequestChainHeader'; const addServiceChainHeader = ({ ctx }: { ctx: NextPageContext }) => { ctx.res?.setHeader( diff --git a/ws-nextjs-app/utilities/addVaryHeader/index.ts b/ws-nextjs-app/utilities/addVaryHeader/index.ts index aefc634fd00..1dcd16ec5ed 100644 --- a/ws-nextjs-app/utilities/addVaryHeader/index.ts +++ b/ws-nextjs-app/utilities/addVaryHeader/index.ts @@ -1,6 +1,6 @@ +import { NextPageContext } from 'next/types'; import { ServerSideExperiment } from '#app/models/types/global'; import { getExperimentVaryHeaders } from '#src/server/utilities/experimentHeader'; -import { NextPageContext } from 'next/types'; const addVaryHeaders = ({ ctx, diff --git a/ws-nextjs-app/utilities/handleServerLogging/index.ts b/ws-nextjs-app/utilities/handleServerLogging/index.ts index e88fd9b4ef3..96631f86a07 100644 --- a/ws-nextjs-app/utilities/handleServerLogging/index.ts +++ b/ws-nextjs-app/utilities/handleServerLogging/index.ts @@ -1,10 +1,10 @@ +import { DocumentContext } from 'next/document'; import { SERVER_SIDE_RENDER_REQUEST_RECEIVED, SERVER_SIDE_REQUEST_FAILED, } from '#app/lib/logger.const'; import { INTERNAL_SERVER_ERROR, OK } from '#app/lib/statusCodes.const'; import nodeLogger from '#lib/logger.node'; -import { DocumentContext } from 'next/document'; import { PageTypes } from '#app/models/types/global'; import sendCustomMetric from '#src/server/utilities/customMetrics'; From 698f3ccfca6f60c66244cc8a56e8ab1408c6425f Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 16 Dec 2025 20:38:14 +0000 Subject: [PATCH 71/91] Update _app.page.tsx --- ws-nextjs-app/pages/_app.page.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index e1319ef34ca..5515296fc28 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -60,9 +60,9 @@ export default class CustomApp extends App { // The 'pageProps' returned are passed down to ALL pages and merged with page // specific 'pageProps' from their getInitialProps / getServerSideProps functions static async getInitialProps({ ctx }: AppContext) { - const { asPath } = ctx; + const { asPath = '' } = ctx; - const { isApp, isAmp, isLite } = getPathExtension(asPath || ''); + const { isApp, isAmp, isLite } = getPathExtension(asPath); const routeSegments = asPath?.split('/')?.filter(Boolean); @@ -71,8 +71,7 @@ export default class CustomApp extends App { const toggles = await getToggles(service); const pageType = - (ctx.req?.headers['page-type'] as PageTypes) || - derivePageType(asPath || ''); + (ctx.req?.headers['page-type'] as PageTypes) || derivePageType(asPath); const serverSideExperiments = getServerExperiments({ headers: ctx.req?.headers || {}, From 5966ef7e0dc28dd45eb76edecbbe4582604b99bb Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 16 Dec 2025 20:41:03 +0000 Subject: [PATCH 72/91] Update _app.page.tsx --- ws-nextjs-app/pages/_app.page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 5515296fc28..033dadc9c76 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -1,4 +1,4 @@ -import App, { AppContext, AppProps } from 'next/app'; +import App, { AppContext } from 'next/app'; import { ATIData } from '#app/components/ATIAnalytics/types'; import ThemeProvider from '#app/components/ThemeProvider'; import { ToggleContextProvider } from '#app/contexts/ToggleContext'; @@ -25,7 +25,7 @@ import addServiceChainHeader from '#nextjs/utilities/addServiceChainHeader'; import addOnionLocationHeader from '#nextjs/utilities/addOnionLocationHeader'; import addVaryHeaders from '#nextjs/utilities/addVaryHeader'; -interface Props extends AppProps { +interface Props { pageProps: { bbcOrigin?: string; id?: string; From da310b10faac12708dcc7dd309f67d1b4a5c91b5 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 09:42:53 +0000 Subject: [PATCH 73/91] Update index.test.ts --- .../utilities/derivePageType/index.test.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ws-nextjs-app/utilities/derivePageType/index.test.ts b/ws-nextjs-app/utilities/derivePageType/index.test.ts index 4671bff7517..f12bfcca4b3 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.test.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.test.ts @@ -8,51 +8,51 @@ import { import derivePageType from '.'; describe('derivePageType', () => { + it('should strip our query params from the pathname', () => { + const pathname = '/pidgin/live/xxxxxxxxx?foo=bar'; + const result = derivePageType(pathname); + expect(result).toEqual(LIVE_PAGE); + }); + it("should return LIVE_PAGE if pathname includes 'live'", () => { - const pathname = '/burmese/live/xxxxxxxxx'; + const pathname = '/pidgin/live/xxxxxxxxx'; const result = derivePageType(pathname); expect(result).toEqual(LIVE_PAGE); }); it("should return UGC_PAGE if pathname includes 'send'", () => { - const pathname = '/burmese/send/xxxxxxxxx'; + const pathname = '/pidgin/send/xxxxxxxxx'; const result = derivePageType(pathname); expect(result).toEqual(UGC_PAGE); }); it("should return AV_EMBEDS if pathname includes 'av-embeds'", () => { - const pathname = '/burmese/av-embeds/xxxxxxxxx'; + const pathname = '/pidgin/av-embeds/xxxxxxxxx'; const result = derivePageType(pathname); expect(result).toEqual(AV_EMBEDS); }); it("should return DOWNLOADS_PAGE if pathname includes 'downloads'", () => { - const pathname = '/burmese/downloads/xxxxxxxxx'; + const pathname = '/korean/downloads'; const result = derivePageType(pathname); expect(result).toEqual(DOWNLOADS_PAGE); }); it('should return ARTICLE_PAGE if pathname matches Optimo ID pattern', () => { - const pathname = '/burmese/articles/c0000000000o'; + const pathname = '/pidgin/articles/c0000000000o'; const result = derivePageType(pathname); expect(result).toEqual(ARTICLE_PAGE); }); it('should return ARTICLE_PAGE if pathname matches CPS ID pattern', () => { - const pathname = '/burmese/instituional-1234567'; + const pathname = '/pidgin/instituional-1234567'; const result = derivePageType(pathname); expect(result).toEqual(ARTICLE_PAGE); }); it('should return Unknown if pathname does not include live or send', () => { - const pathname = '/burmese/xxxxxxxxx'; + const pathname = '/pidgin/xxxxxxxxx'; const result = derivePageType(pathname); expect(result).toEqual('Unknown'); }); - - it('should strip our query params from the pathname', () => { - const pathname = '/burmese/live/xxxxxxxxx?foo=bar'; - const result = derivePageType(pathname); - expect(result).toEqual(LIVE_PAGE); - }); }); From e894ff330ead2bffb435b6978b586aa302704d10 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 09:52:43 +0000 Subject: [PATCH 74/91] Convert _error page to class component --- ws-nextjs-app/pages/_error.page.tsx | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/ws-nextjs-app/pages/_error.page.tsx b/ws-nextjs-app/pages/_error.page.tsx index 5a6b0002fa7..b2faeb05c45 100644 --- a/ws-nextjs-app/pages/_error.page.tsx +++ b/ws-nextjs-app/pages/_error.page.tsx @@ -2,21 +2,24 @@ import { NextPageContext } from 'next'; import NextError from 'next/error'; import { NOT_FOUND } from '#app/lib/statusCodes.const'; -function Error({ statusCode }: { statusCode: number }) { - return ; -} +class Error extends React.Component<{ statusCode: number }> { + static getInitialProps({ res, err }: NextPageContext) { + let statusCode = NOT_FOUND; -Error.getInitialProps = ({ res, err }: NextPageContext) => { - let statusCode = NOT_FOUND; + if (res) { + statusCode = res.statusCode; + } else if (err) { + statusCode = + typeof err.statusCode === 'number' ? err.statusCode : statusCode; + } - if (res) { - statusCode = res.statusCode; - } else if (err) { - statusCode = - typeof err.statusCode === 'number' ? err.statusCode : statusCode; + return { statusCode }; } - return { statusCode }; -}; + render() { + const { statusCode } = this.props; + return ; + } +} export default Error; From 49848a2a079955d639eea725f721d66bcbef80de Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 10:07:46 +0000 Subject: [PATCH 75/91] Update _error.page.tsx --- ws-nextjs-app/pages/_error.page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ws-nextjs-app/pages/_error.page.tsx b/ws-nextjs-app/pages/_error.page.tsx index b2faeb05c45..ec7a8a176d9 100644 --- a/ws-nextjs-app/pages/_error.page.tsx +++ b/ws-nextjs-app/pages/_error.page.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import { NextPageContext } from 'next'; import NextError from 'next/error'; import { NOT_FOUND } from '#app/lib/statusCodes.const'; From dee6b14c4d546901173995bdbbe593eb424ee7ed Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 11:22:29 +0000 Subject: [PATCH 76/91] Use `UNKNOWN_PAGE` const --- ws-nextjs-app/utilities/derivePageType/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/utilities/derivePageType/index.ts b/ws-nextjs-app/utilities/derivePageType/index.ts index 6dd5785e4aa..d9545f0ff7a 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.ts @@ -6,6 +6,7 @@ import { LIVE_PAGE, UGC_PAGE, HOME_PAGE, + UNKNOWN_PAGE, } from '#app/routes/utils/pageTypes'; import { isOptimoIdCheck, @@ -52,5 +53,5 @@ export default function derivePageType(pathname: string): PageTypes { if (isOptimoIdCheck(sanitisedPathname)) return ARTICLE_PAGE; if (isCpsIdCheck(sanitisedPathname)) return ARTICLE_PAGE; - return 'Unknown'; + return UNKNOWN_PAGE; } From a7475a614c6d3d99c7bcc7c385e7eff783fbc742 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 11:22:49 +0000 Subject: [PATCH 77/91] Mock url typo --- ws-nextjs-app/utilities/derivePageType/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/utilities/derivePageType/index.test.ts b/ws-nextjs-app/utilities/derivePageType/index.test.ts index 024d33188b9..9415b0870f2 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.test.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.test.ts @@ -58,7 +58,7 @@ describe('derivePageType', () => { }); it('should return ARTICLE_PAGE if pathname matches CPS ID pattern', () => { - const pathname = '/pidgin/instituional-1234567'; + const pathname = '/pidgin/institutional-1234567'; const result = derivePageType(pathname); expect(result).toEqual(ARTICLE_PAGE); }); From e855f901e52eecb33be0f89b0ee3a934845802ca Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 11:25:54 +0000 Subject: [PATCH 78/91] Check localhost domain using `some` --- ws-nextjs-app/utilities/addCspHeader/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/utilities/addCspHeader/index.ts b/ws-nextjs-app/utilities/addCspHeader/index.ts index b211300bf36..0b0b843baad 100644 --- a/ws-nextjs-app/utilities/addCspHeader/index.ts +++ b/ws-nextjs-app/utilities/addCspHeader/index.ts @@ -46,7 +46,9 @@ type AddCspHeaderProps = { const addCspHeader = ({ ctx, service, toggles }: AddCspHeaderProps) => { const hostname = ctx.req?.headers.host || ''; - const isLocalhost = LOCALHOST_DOMAINS.includes(hostname.split(':')?.[0]); + const isLocalhost = LOCALHOST_DOMAINS.some(domain => + hostname.includes(domain), + ); const PRODUCTION_ONLY = !isLocalhost && process.env.NODE_ENV === 'production'; From 4d018e4c1e1a5652b731f9125a90c836b9558615 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 11:28:49 +0000 Subject: [PATCH 79/91] Change instances of `'unknown'` page type to use const --- src/app/components/ChartbeatAnalytics/utils/index.test.ts | 3 ++- src/server/index.jsx | 3 ++- ws-nextjs-app/utilities/derivePageType/index.test.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/app/components/ChartbeatAnalytics/utils/index.test.ts b/src/app/components/ChartbeatAnalytics/utils/index.test.ts index 4e396e6ea0b..d071a7d5b20 100644 --- a/src/app/components/ChartbeatAnalytics/utils/index.test.ts +++ b/src/app/components/ChartbeatAnalytics/utils/index.test.ts @@ -13,6 +13,7 @@ import { AUDIO_PAGE, TV_PAGE, LIVE_TV_PAGE, + UNKNOWN_PAGE, } from '../../../routes/utils/pageTypes'; import { chartbeatUID, @@ -309,7 +310,7 @@ describe('Chartbeat utilities', () => { ${MEDIA_ASSET_PAGE} | ${'MAP Page Title'} | ${'MAP Page Title'} ${ARTICLE_PAGE} | ${'Article Page Title'} | ${'Article Page Title'} ${MEDIA_ARTICLE_PAGE} | ${'Media Article Page Title'} | ${'Media Article Page Title'} - ${'unknown'} | ${'Unknown Page Title'} | ${'Unknown Page Title'} + ${UNKNOWN_PAGE} | ${'Unknown Page Title'} | ${'Unknown Page Title'} `( 'should return correct title when pageType is $pageType', ({ pageType, title, expected }) => { diff --git a/src/server/index.jsx b/src/server/index.jsx index 55091016365..a5059e73381 100644 --- a/src/server/index.jsx +++ b/src/server/index.jsx @@ -41,6 +41,7 @@ import extractHeaders from './utilities/extractHeaders'; import addPlatformToRequestChainHeader from './utilities/addPlatformToRequestChainHeader'; import services from './utilities/serviceConfigs'; import createAdNonce from '../app/utilities/createAdNonce'; +import { UNKNOWN_PAGE } from '../app/routes/utils/pageTypes'; const morgan = require('morgan'); @@ -198,7 +199,7 @@ server.get( injectPlatformToRequestChainHeader, ], async ({ url, query, headers, path: urlPath }, res) => { - let derivedPageType = 'Unknown'; + let derivedPageType = UNKNOWN_PAGE; let serverSideExperiments = []; try { diff --git a/ws-nextjs-app/utilities/derivePageType/index.test.ts b/ws-nextjs-app/utilities/derivePageType/index.test.ts index 9415b0870f2..938a187faf5 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.test.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.test.ts @@ -5,6 +5,7 @@ import { DOWNLOADS_PAGE, ARTICLE_PAGE, HOME_PAGE, + UNKNOWN_PAGE, } from '#app/routes/utils/pageTypes'; import derivePageType from '.'; @@ -66,6 +67,6 @@ describe('derivePageType', () => { it('should return Unknown if pathname does not include live or send', () => { const pathname = '/pidgin/xxxxxxxxx'; const result = derivePageType(pathname); - expect(result).toEqual('Unknown'); + expect(result).toEqual(UNKNOWN_PAGE); }); }); From 1e822b3cf539500383cab996602f2da356762467 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 11:57:10 +0000 Subject: [PATCH 80/91] Update _error.page.tsx --- ws-nextjs-app/pages/_error.page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/pages/_error.page.tsx b/ws-nextjs-app/pages/_error.page.tsx index ec7a8a176d9..c620cd30f5f 100644 --- a/ws-nextjs-app/pages/_error.page.tsx +++ b/ws-nextjs-app/pages/_error.page.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import { Component } from 'react'; import { NextPageContext } from 'next'; import NextError from 'next/error'; import { NOT_FOUND } from '#app/lib/statusCodes.const'; -class Error extends React.Component<{ statusCode: number }> { +class Error extends Component<{ statusCode: number }> { static getInitialProps({ res, err }: NextPageContext) { let statusCode = NOT_FOUND; From 4ccd313872611dba67dbb34acc944980173c9942 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 17 Dec 2025 11:58:06 +0000 Subject: [PATCH 81/91] Make type import consistent --- ws-nextjs-app/pages/_error.page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/pages/_error.page.tsx b/ws-nextjs-app/pages/_error.page.tsx index c620cd30f5f..100560e5fe7 100644 --- a/ws-nextjs-app/pages/_error.page.tsx +++ b/ws-nextjs-app/pages/_error.page.tsx @@ -1,5 +1,5 @@ import { Component } from 'react'; -import { NextPageContext } from 'next'; +import { NextPageContext } from 'next/types'; import NextError from 'next/error'; import { NOT_FOUND } from '#app/lib/statusCodes.const'; From 3188751bdeb3e44e874f47796f1c58ce20a69e42 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 30 Dec 2025 17:04:17 +0000 Subject: [PATCH 82/91] un-pluralise vary header function --- ws-nextjs-app/pages/_app.page.tsx | 6 ++++-- ws-nextjs-app/utilities/addVaryHeader/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ws-nextjs-app/pages/_app.page.tsx b/ws-nextjs-app/pages/_app.page.tsx index 033dadc9c76..1edb7adee1f 100644 --- a/ws-nextjs-app/pages/_app.page.tsx +++ b/ws-nextjs-app/pages/_app.page.tsx @@ -23,7 +23,8 @@ import addCspHeader from '#nextjs/utilities/addCspHeader'; import derivePageType from '#nextjs/utilities/derivePageType'; import addServiceChainHeader from '#nextjs/utilities/addServiceChainHeader'; import addOnionLocationHeader from '#nextjs/utilities/addOnionLocationHeader'; -import addVaryHeaders from '#nextjs/utilities/addVaryHeader'; +import addVaryHeader from '#nextjs/utilities/addVaryHeader'; +import addLinkHeader from '#nextjs/utilities/addLinkHeader'; interface Props { pageProps: { @@ -82,7 +83,8 @@ export default class CustomApp extends App { addServiceChainHeader({ ctx }); addCspHeader({ ctx, service, toggles }); addOnionLocationHeader({ ctx }); - addVaryHeaders({ ctx, serverSideExperiments }); + addVaryHeader({ ctx, serverSideExperiments }); + addLinkHeader({ ctx }); return { pageProps: { diff --git a/ws-nextjs-app/utilities/addVaryHeader/index.ts b/ws-nextjs-app/utilities/addVaryHeader/index.ts index 1dcd16ec5ed..b1dafce00cb 100644 --- a/ws-nextjs-app/utilities/addVaryHeader/index.ts +++ b/ws-nextjs-app/utilities/addVaryHeader/index.ts @@ -2,7 +2,7 @@ import { NextPageContext } from 'next/types'; import { ServerSideExperiment } from '#app/models/types/global'; import { getExperimentVaryHeaders } from '#src/server/utilities/experimentHeader'; -const addVaryHeaders = ({ +const addVaryHeader = ({ ctx, serverSideExperiments, }: { @@ -17,4 +17,4 @@ const addVaryHeaders = ({ ctx.res?.setHeader('Vary', allVaryHeaders); }; -export default addVaryHeaders; +export default addVaryHeader; From 94dd57597803c7ca81856e73d1587c5e4363eb0b Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 30 Dec 2025 17:04:25 +0000 Subject: [PATCH 83/91] Add `Link` header function --- .../utilities/addLinkHeader/index.test.ts | 43 +++++++++++++++++++ .../utilities/addLinkHeader/index.ts | 21 +++++++++ 2 files changed, 64 insertions(+) create mode 100644 ws-nextjs-app/utilities/addLinkHeader/index.test.ts create mode 100644 ws-nextjs-app/utilities/addLinkHeader/index.ts diff --git a/ws-nextjs-app/utilities/addLinkHeader/index.test.ts b/ws-nextjs-app/utilities/addLinkHeader/index.test.ts new file mode 100644 index 00000000000..a826ac5b799 --- /dev/null +++ b/ws-nextjs-app/utilities/addLinkHeader/index.test.ts @@ -0,0 +1,43 @@ +import { NextPageContext } from 'next/types'; +import addLinkHeader from '.'; + +describe('addLinkHeader', () => { + const mockSetHeader = jest.fn(); + const mockCtx = { + res: { + setHeader: mockSetHeader, + }, + } as unknown as NextPageContext; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should add the correct Link header for Test env', () => { + process.env.SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN = + 'https://static.test.files.bbci.co.uk'; + + process.env.ATI_BASE_URL = 'https://logws1363.ati-host.net?'; + + addLinkHeader({ ctx: mockCtx }); + + expect(mockSetHeader).toHaveBeenCalledWith( + 'Link', + '; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect"', + ); + }); + + it('should add the correct Link header for Live env', () => { + process.env.SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN = + 'https://static.files.bbci.co.uk'; + + process.env.ATI_BASE_URL = 'https://a1.api.bbc.co.uk/hit.xiti?'; + + addLinkHeader({ ctx: mockCtx }); + + expect(mockSetHeader).toHaveBeenCalledWith( + 'Link', + '; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect",; rel="preconnect"; crossorigin,; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect"', + ); + }); +}); diff --git a/ws-nextjs-app/utilities/addLinkHeader/index.ts b/ws-nextjs-app/utilities/addLinkHeader/index.ts new file mode 100644 index 00000000000..f8552f96426 --- /dev/null +++ b/ws-nextjs-app/utilities/addLinkHeader/index.ts @@ -0,0 +1,21 @@ +import { NextPageContext } from 'next/types'; +import getAssetOrigins from '#server/utilities/getAssetOrigins'; + +const addLinkHeader = ({ ctx }: { ctx: NextPageContext }) => { + const assetOrigins = getAssetOrigins(); + + ctx.res?.setHeader( + 'Link', + assetOrigins + .map(domainName => { + const crossOrigin = + domainName === 'https://static.files.bbci.co.uk' + ? `,<${domainName}>; rel="preconnect"; crossorigin` + : ''; + return `<${domainName}>; rel="dns-prefetch", <${domainName}>; rel="preconnect"${crossOrigin}`; + }) + .join(','), + ); +}; + +export default addLinkHeader; From 2e34c5ffabf9c620bebd501a916f71f693ae4655 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Tue, 30 Dec 2025 17:09:03 +0000 Subject: [PATCH 84/91] Fix typo in process.env setting in test --- ws-nextjs-app/utilities/addLinkHeader/index.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ws-nextjs-app/utilities/addLinkHeader/index.test.ts b/ws-nextjs-app/utilities/addLinkHeader/index.test.ts index a826ac5b799..7232c1845ad 100644 --- a/ws-nextjs-app/utilities/addLinkHeader/index.test.ts +++ b/ws-nextjs-app/utilities/addLinkHeader/index.test.ts @@ -17,7 +17,7 @@ describe('addLinkHeader', () => { process.env.SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN = 'https://static.test.files.bbci.co.uk'; - process.env.ATI_BASE_URL = 'https://logws1363.ati-host.net?'; + process.env.SIMORGH_ATI_BASE_URL = 'https://logws1363.ati-host.net?'; addLinkHeader({ ctx: mockCtx }); @@ -31,13 +31,13 @@ describe('addLinkHeader', () => { process.env.SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN = 'https://static.files.bbci.co.uk'; - process.env.ATI_BASE_URL = 'https://a1.api.bbc.co.uk/hit.xiti?'; + process.env.SIMORGH_ATI_BASE_URL = 'https://a1.api.bbc.co.uk/hit.xiti?'; addLinkHeader({ ctx: mockCtx }); expect(mockSetHeader).toHaveBeenCalledWith( 'Link', - '; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect",; rel="preconnect"; crossorigin,; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect"', + '; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect",; rel="preconnect"; crossorigin,; rel="dns-prefetch", ; rel="preconnect",; rel="dns-prefetch", ; rel="preconnect"', ); }); }); From 8082e920ca4c85421f250ff64b1120d84423f3ca Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 31 Dec 2025 09:36:44 +0000 Subject: [PATCH 85/91] Change `getAssetOrigins` to TS --- .../utilities/getAssetOrigins/{index.test.js => index.test.ts} | 0 src/server/utilities/getAssetOrigins/{index.js => index.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/server/utilities/getAssetOrigins/{index.test.js => index.test.ts} (100%) rename src/server/utilities/getAssetOrigins/{index.js => index.ts} (100%) diff --git a/src/server/utilities/getAssetOrigins/index.test.js b/src/server/utilities/getAssetOrigins/index.test.ts similarity index 100% rename from src/server/utilities/getAssetOrigins/index.test.js rename to src/server/utilities/getAssetOrigins/index.test.ts diff --git a/src/server/utilities/getAssetOrigins/index.js b/src/server/utilities/getAssetOrigins/index.ts similarity index 100% rename from src/server/utilities/getAssetOrigins/index.js rename to src/server/utilities/getAssetOrigins/index.ts From 7321d43a6fca7c95459e262ee26ce480c6667282 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Wed, 31 Dec 2025 09:37:14 +0000 Subject: [PATCH 86/91] Move consts out of function --- src/server/utilities/getAssetOrigins/index.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/server/utilities/getAssetOrigins/index.ts b/src/server/utilities/getAssetOrigins/index.ts index 4e2e7c478a4..61abd714100 100644 --- a/src/server/utilities/getAssetOrigins/index.ts +++ b/src/server/utilities/getAssetOrigins/index.ts @@ -1,8 +1,7 @@ -const getAssetOrigins = () => { - const IMAGES_ORIGIN = 'https://ichef.bbci.co.uk'; - - const ANALYTICS_ORIGINS = ['https://ping.chartbeat.net']; +const IMAGES_ORIGIN = 'https://ichef.bbci.co.uk'; +const ANALYTICS_ORIGINS = ['https://ping.chartbeat.net']; +const getAssetOrigins = () => { const assetOrigins = [ IMAGES_ORIGIN, process.env.SIMORGH_PUBLIC_STATIC_ASSETS_ORIGIN, From baa8fba7930056dd14e33dab725cd52a88316cde Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Mon, 5 Jan 2026 14:35:36 +0000 Subject: [PATCH 87/91] Update ws-nextjs-app/utilities/derivePageType/index.test.ts Co-authored-by: Harvey Peachey --- ws-nextjs-app/utilities/derivePageType/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/utilities/derivePageType/index.test.ts b/ws-nextjs-app/utilities/derivePageType/index.test.ts index 938a187faf5..8b7a20dda5c 100644 --- a/ws-nextjs-app/utilities/derivePageType/index.test.ts +++ b/ws-nextjs-app/utilities/derivePageType/index.test.ts @@ -10,7 +10,7 @@ import { import derivePageType from '.'; describe('derivePageType', () => { - it('should strip our query params from the pathname', () => { + it('should strip out query params from the pathname', () => { const pathname = '/pidgin/live/xxxxxxxxx?foo=bar'; const result = derivePageType(pathname); expect(result).toEqual(LIVE_PAGE); From 14de2bd839c6b6c813ed7c5a67f915bc3758cdff Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Mon, 5 Jan 2026 17:47:05 +0000 Subject: [PATCH 88/91] Update responseHeaderTests.ts --- ws-nextjs-app/integration/utils/responseHeaderTests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ws-nextjs-app/integration/utils/responseHeaderTests.ts b/ws-nextjs-app/integration/utils/responseHeaderTests.ts index e3b3096a648..21feba72d44 100644 --- a/ws-nextjs-app/integration/utils/responseHeaderTests.ts +++ b/ws-nextjs-app/integration/utils/responseHeaderTests.ts @@ -1,6 +1,6 @@ export default () => { - // This header is set in the Next.js middleware/proxy file: 'ws-nextjs-app/middleware.ts' - // The presence of this header and its value including the word 'SIMORGH' indicates that middleware/proxy is working correctly + // This header is set in the Next.js _app.page.tsx file + // The presence of this header and its value including the word 'SIMORGH' indicates that _app setting headers correctly describe('req-svc-chain is set correctly', () => { it('should contain the correct svc chain', async () => { const fetchResponse = await fetch(window.location.href); From 04b9534684662806583b928ac9ab23011388659f Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Mon, 5 Jan 2026 17:47:42 +0000 Subject: [PATCH 89/91] typo --- ws-nextjs-app/integration/utils/responseHeaderTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ws-nextjs-app/integration/utils/responseHeaderTests.ts b/ws-nextjs-app/integration/utils/responseHeaderTests.ts index 21feba72d44..4835be75c7b 100644 --- a/ws-nextjs-app/integration/utils/responseHeaderTests.ts +++ b/ws-nextjs-app/integration/utils/responseHeaderTests.ts @@ -1,6 +1,6 @@ export default () => { // This header is set in the Next.js _app.page.tsx file - // The presence of this header and its value including the word 'SIMORGH' indicates that _app setting headers correctly + // The presence of this header and its value including the word 'SIMORGH' indicates that _app.page.tsx is setting headers correctly describe('req-svc-chain is set correctly', () => { it('should contain the correct svc chain', async () => { const fetchResponse = await fetch(window.location.href); From ad04159c6fbccded353b4558928c964e3cbc125e Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Mon, 5 Jan 2026 18:29:13 +0000 Subject: [PATCH 90/91] Extend `responseHeaderTests` integration tests - CSP can't be fully tested as integration uses localhost for the domain and CSP headers aren't set on localhost --- .../integration/utils/responseHeaderTests.ts | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/ws-nextjs-app/integration/utils/responseHeaderTests.ts b/ws-nextjs-app/integration/utils/responseHeaderTests.ts index 4835be75c7b..9a554a4ccbc 100644 --- a/ws-nextjs-app/integration/utils/responseHeaderTests.ts +++ b/ws-nextjs-app/integration/utils/responseHeaderTests.ts @@ -1,5 +1,5 @@ export default () => { - // This header is set in the Next.js _app.page.tsx file + // These headers are set in the Next.js _app.page.tsx file // The presence of this header and its value including the word 'SIMORGH' indicates that _app.page.tsx is setting headers correctly describe('req-svc-chain is set correctly', () => { it('should contain the correct svc chain', async () => { @@ -8,4 +8,40 @@ export default () => { expect(reqSvcChain).toContain('SIMORGH'); }); }); + + // describe('CSP header is set correctly', () => { + // it('should contain the correct Content-Security-Policy header', async () => { + // const fetchResponse = await fetch(window.location.href); + // const cspHeader = fetchResponse.headers.get('Content-Security-Policy'); + // expect(cspHeader).toContain("default-src 'self'"); + // }); + // }); + + describe('Onion-Location header is set correctly', () => { + it('should contain the correct Onion-Location header', async () => { + const fetchResponse = await fetch(window.location.href); + const onionHeader = fetchResponse.headers.get('Onion-Location'); + expect(onionHeader).toBe( + `https://www.bbcweb3hytmzhn5d532owbu6oqadra5z3ar726vq5kgwwn6aucdccrad.onion${new URL(window.location.href).pathname}`, + ); + }); + }); + + describe('Vary header is set correctly', () => { + it('should contain the correct Vary header', async () => { + const fetchResponse = await fetch(window.location.href); + const varyHeader = fetchResponse.headers.get('Vary'); + expect(varyHeader).toContain('X-Country, Accept-Encoding'); + }); + }); + + describe('Link header is set correctly', () => { + it('should contain the correct Link header for AMP pages', async () => { + const fetchResponse = await fetch(window.location.href); + const linkHeader = fetchResponse.headers.get('Link'); + expect(linkHeader).toContain( + '; rel="dns-prefetch"', + ); + }); + }); }; From d877e2505791e5fb5e50606ac0fd18bebf0543d4 Mon Sep 17 00:00:00 2001 From: Aaron Moore Date: Mon, 5 Jan 2026 18:29:19 +0000 Subject: [PATCH 91/91] Update responseHeaderTests.ts --- ws-nextjs-app/integration/utils/responseHeaderTests.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/ws-nextjs-app/integration/utils/responseHeaderTests.ts b/ws-nextjs-app/integration/utils/responseHeaderTests.ts index 9a554a4ccbc..d8428604599 100644 --- a/ws-nextjs-app/integration/utils/responseHeaderTests.ts +++ b/ws-nextjs-app/integration/utils/responseHeaderTests.ts @@ -9,14 +9,6 @@ export default () => { }); }); - // describe('CSP header is set correctly', () => { - // it('should contain the correct Content-Security-Policy header', async () => { - // const fetchResponse = await fetch(window.location.href); - // const cspHeader = fetchResponse.headers.get('Content-Security-Policy'); - // expect(cspHeader).toContain("default-src 'self'"); - // }); - // }); - describe('Onion-Location header is set correctly', () => { it('should contain the correct Onion-Location header', async () => { const fetchResponse = await fetch(window.location.href);