Skip to content
Draft
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
493f7bd
WS-1400: Add client side redirect
shayneahchoon Nov 11, 2025
717d0bb
Merge branch 'latest' into WS-1400-ECT-HELMET
shayneahchoon Nov 11, 2025
01d4e4d
Merge branch 'latest' into WS-1400-ECT-HELMET
shayneahchoon Nov 12, 2025
c994aba
WS-1533: Update packages with 7 day maturity threshold
shayneahchoon Nov 12, 2025
2bea896
Merge branch 'WS-1400-ECT-HELMET' of github.com:bbc/simorgh into WS-1…
shayneahchoon Nov 12, 2025
70e7ad7
Merge branch 'latest' into WS-1400-ECT-HELMET
shayneahchoon Nov 12, 2025
6359b3a
WS-1533: Update packages with 7 day maturity threshold
shayneahchoon Nov 12, 2025
15f88f5
Merge branch 'WS-1400-ECT-HELMET' of github.com:bbc/simorgh into WS-1…
shayneahchoon Nov 12, 2025
d5c84a4
WS-1533: Update packages with 7 day maturity threshold
shayneahchoon Nov 12, 2025
9bac75e
WS-1533: Update packages with 7 day maturity threshold
shayneahchoon Nov 12, 2025
1e086de
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
4cdf217
Merge branch 'latest' into WS-1400-ECT-HELMET
shayneahchoon Nov 17, 2025
f01ad51
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
a606199
Merge branch 'WS-1400-ECT-HELMET' of github.com:bbc/simorgh into WS-1…
shayneahchoon Nov 17, 2025
4dfd6cc
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
6832436
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
dc3594d
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
aa80aaf
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
2a2be1c
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
e0dbde9
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
5f2df6e
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
97dfa11
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
48451c3
WS-1399: Add client side redirect
shayneahchoon Nov 17, 2025
1571678
Merge branch 'latest' into WS-1400-ECT-HELMET
shayneahchoon Nov 24, 2025
83a1a3b
WS-1737: Add tracking for redirect
shayneahchoon Nov 24, 2025
657b4c2
Merge branch 'WS-1400-ECT-HELMET' of github.com:bbc/simorgh into WS-1…
shayneahchoon Nov 24, 2025
501c8cc
WS-1737: Add tracking for redirect
shayneahchoon Nov 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/hooks/useNetworkStatusTracker/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { renderHook, act } from '@testing-library/react';
import { renderHook as renderSSRHook } from '@testing-library/react-hooks/server';
import { EffectiveNetworkType } from '#app/models/types/global';
import useNetworkStatus from './index';
import { EffectiveNetworkType } from './type';

describe('useNetworkStatus', () => {
const originalNavigator = window.navigator;
Expand Down
12 changes: 2 additions & 10 deletions src/app/hooks/useNetworkStatusTracker/index.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
import { useState, useEffect } from 'react';
import getEffectiveNetworkType from '#app/lib/utilities/getEffectiveNetworkType';
import { EffectiveNetworkType, NetworkStatus } from './type';
import { NetworkStatus } from './type';

/**
* A hook to monitor and provide real-time network connectivity status.
* Tracks whether the user is online or offline and includes the effective network type.
* @returns {NetworkStatus} An object containing isOnline (boolean), source ('browser'), and networkType (EffectiveNetworkType).
*/

interface NavigatorWithConnection extends Navigator {
connection?: {
effectiveType?: EffectiveNetworkType;
addEventListener?: (type: string, listener: () => void) => void;
removeEventListener?: (type: string, listener: () => void) => void;
};
}

const useNetworkStatusTracker = (): NetworkStatus => {
const [networkStatus, setNetworkStatus] = useState<NetworkStatus>(() => {
const isOnline =
Expand Down Expand Up @@ -58,7 +50,7 @@ const useNetworkStatusTracker = (): NetworkStatus => {
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);

const { connection } = navigator as NavigatorWithConnection;
const { connection } = navigator;

if (connection?.addEventListener) {
connection.addEventListener('change', handleConnectionChange);
Expand Down
8 changes: 1 addition & 7 deletions src/app/hooks/useNetworkStatusTracker/type.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
export type EffectiveNetworkType =
| 'slow-2g'
| '2g'
| '3g'
| '4g'
| '5g'
| 'unknown';
import { EffectiveNetworkType } from '#app/models/types/global';

export type NetworkStatus = {
isOnline: boolean;
Expand Down
2 changes: 1 addition & 1 deletion src/app/lib/utilities/getEffectiveNetworkType.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EffectiveNetworkType } from '../../hooks/useNetworkStatusTracker/type';
import { EffectiveNetworkType } from '#app/models/types/global';

const getEffectiveNetworkType = (): EffectiveNetworkType => {
if (typeof window === 'undefined' || !navigator) {
Expand Down
8 changes: 8 additions & 0 deletions src/app/models/types/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,11 @@ export type ServicesVariantsProps = {
service: Services;
variant?: Variants;
};

export type EffectiveNetworkType =
| 'slow-2g'
| '2g'
| '3g'
| '4g'
| '5g'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not convinced we'll get 5g through as per the docs - https://developer.mozilla.org/en-US/docs/Glossary/Effective_connection_type (though I know that was there before)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, I put this one here because it partially clashed with the hook that Neon are working on. I don't want to make any changes in case it affects their work.

| 'unknown';
8 changes: 8 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { ReverbClient } from '#models/types/eventTracking';
import { BumpType, Player } from '#app/components/MediaLoader/types';
import { EffectiveNetworkType } from '#app/models/types/global';

declare global {
interface Navigator {
connection?: {
effectiveType?: EffectiveNetworkType;
addEventListener?: (type: string, listener: () => void) => void;
removeEventListener?: (type: string, listener: () => void) => void;
};
}
interface Window {
bbcpage:
| {
Expand Down
2 changes: 2 additions & 0 deletions src/server/Document/Renderers/CanonicalRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import IfAboveIE9 from '#app/legacy/components/IfAboveIE9Comment';
import NO_JS_CLASSNAME from '#app/lib/noJs.const';
import { getProcessEnvAppVariables } from '#app/lib/utilities/getEnvConfig';
import serialiseForScript from '#app/lib/utilities/serialiseForScript';
import CanonicalToLiteRedirect from '#src/server/utilities/CanonicalToLiteRedirect';
import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript';
import { BaseRendererProps } from './types';
import ReverbTemplate from './ReverbTemplate';
Expand Down Expand Up @@ -98,6 +99,7 @@ export default function CanonicalRenderer({
return (
<html lang="en-GB" className={NO_JS_CLASSNAME} {...htmlAttrs}>
<head>
<CanonicalToLiteRedirect />
<ReverbTemplate nonce={nonce} />
{isApp && <meta name="robots" content="noindex" />}
{title}
Expand Down
50 changes: 50 additions & 0 deletions src/server/Document/__snapshots__/component.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,31 @@ exports[`Document Component should render APP version correctly 1`] = `
href="modern.igbo.js"
rel="modulepreload"
/>
<script>

window.addEventListener('load', () =&gt; {
(function redirectScript(window) {
var _window$navigator;
var pathname = window.location.pathname;
var allowList = ['/pidgin/articles/czrzwn80zjmo'];
if (window !== null && window !== void 0 && (_window$navigator = window.navigator) !== null && _window$navigator !== void 0 && (_window$navigator = _window$navigator.connection) !== null && _window$navigator !== void 0 && _window$navigator.effectiveType && allowList.includes(pathname)) {
var toLitePath = "".concat(pathname, ".lite");
var ect = window.navigator.connection.effectiveType;
var normalisedEct = ect.toLocaleLowerCase();
switch (normalisedEct) {
case 'slow-2g':
case '2g':
case '3g':
window.location.replace(toLitePath);
break;
default:
break;
}
}
})(window)
})

</script>
<script>

window.__reverb = {};
Expand Down Expand Up @@ -320,6 +345,31 @@ exports[`Document Component should render correctly 1`] = `
href="modern.igbo.js"
rel="modulepreload"
/>
<script>

window.addEventListener('load', () =&gt; {
(function redirectScript(window) {
var _window$navigator;
var pathname = window.location.pathname;
var allowList = ['/pidgin/articles/czrzwn80zjmo'];
if (window !== null && window !== void 0 && (_window$navigator = window.navigator) !== null && _window$navigator !== void 0 && (_window$navigator = _window$navigator.connection) !== null && _window$navigator !== void 0 && _window$navigator.effectiveType && allowList.includes(pathname)) {
var toLitePath = "".concat(pathname, ".lite");
var ect = window.navigator.connection.effectiveType;
var normalisedEct = ect.toLocaleLowerCase();
switch (normalisedEct) {
case 'slow-2g':
case '2g':
case '3g':
window.location.replace(toLitePath);
break;
default:
break;
}
}
})(window)
})

</script>
<script>

window.__reverb = {};
Expand Down
50 changes: 50 additions & 0 deletions src/server/utilities/CanonicalToLiteRedirect/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { redirectScript } from '.';

describe('LiteRedirect', () => {
it.each([
{
effectiveType: 'randomValue',
expectedRedirect: false,
},
{
effectiveType: 'slow-2g',
expectedRedirect: true,
},
{
effectiveType: '2g',
expectedRedirect: true,
},
{
effectiveType: '3g',
expectedRedirect: true,
},
{
effectiveType: '4g',
expectedRedirect: false,
},
])(
`When the client is on $effectiveType then expect redirect should be $expectRedirect`,
({ effectiveType, expectedRedirect }) => {
const mockWindow = {
navigator: {
connection: {
effectiveType,
},
},
location: {
replace: jest.fn(),
href: 'https://www.somepath.com/',
pathname: '/pidgin/articles/czrzwn80zjmo',
},
} as unknown as Window;

redirectScript(mockWindow);
const replaceCallStack = (mockWindow.location.replace as jest.Mock).mock
.calls[0]?.[0];

const hasRedirected = Boolean(replaceCallStack);

expect(hasRedirected).toBe(expectedRedirect);
},
);
});
38 changes: 38 additions & 0 deletions src/server/utilities/CanonicalToLiteRedirect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';

export const redirectScript = (window: Window) => {
const { pathname } = window.location;

const allowList = ['/pidgin/articles/czrzwn80zjmo'];
if (
window?.navigator?.connection?.effectiveType &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Older browsers like Opera Mini don’t support optional chaining I don't think, so they might hit a SyntaxError before the redirect runs (with the redirectScript.toString() injected in the script tag on lines 30-34

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amoore108 any thoughts on this? ^

Copy link
Contributor

@amoore108 amoore108 Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script should be transpiled down by the time its rendered in the browser. Worth checking locally that that is the case.

allowList.includes(pathname)
) {
const toLitePath = `${pathname}.lite`;
const ect = window.navigator.connection.effectiveType;
const normalisedEct = ect.toLocaleLowerCase();
switch (normalisedEct) {
case 'slow-2g':
case '2g':
case '3g':
window.location.replace(toLitePath);
break;
default:
break;
}
}
};

// THIS COMPONENT IS ONLY TO BE USED WITH CANONICAL REDNERERS
// DO NOT USE IT WITH LITE AND AMP RENDERERS
export default () => {
return (
<script>
{`
window.addEventListener('load', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we use https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event instead? The window load will happen after all HTML/JSON/CSS/images load so perhaps too long?

(${redirectScript.toString()})(window)
})
`}
</script>
);
};
2 changes: 2 additions & 0 deletions ws-nextjs-app/pages/_document.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ 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 CanonicalToLiteRedirect from '#src/server/utilities/CanonicalToLiteRedirect';
import addOperaMiniClassScript from '#app/lib/utilities/addOperaMiniClassScript';
import removeSensitiveHeaders from '../utilities/removeSensitiveHeaders';
import derivePageType from '../utilities/derivePageType';
Expand Down Expand Up @@ -190,6 +191,7 @@ export default class AppDocument extends Document<DocProps> {
return (
<Html lang="en-GB" {...htmlAttrs} className={NO_JS_CLASSNAME}>
<Head>
<CanonicalToLiteRedirect />
<ReverbTemplate />
<script
type="text/javascript"
Expand Down
Loading