Repository: guardian/dotcom-rendering
Pull request #13903: implement football more button for when js is disabled
Author: login: marjisound
Labels: name: dotcom-rendering
name: Seen-on-PROD
Created: 2025-05-02T13:57:58Z | Merged: 2025-05-19T08:28:14Z

## What does this change?
This change adds support for a fallback `More` button on the football matches pages for when JavaScript is disabled in the browser.

## Why introduce nextPageNoJsUrl:

There is a convention in frontend routing where for any page route (e.g. /football/fixtures/more/:year/:month/:day), there is also a corresponding .json route (e.g. /football/fixtures/more/:year/:month/:day.json). These .json routes return the full data required to render a page, including config and navigation.

In DCAR the `More` button is using the .json route to fetch more matches. However, this returns far more data than necessary — config, nav, and other unrelated information — when all that is needed is a list of matches to append to the current view.

Upon further investigation, we noticed that the HTML version of the `/more/` routes serves the same content as the equivalent non-/more/ routes. Based on this, we decided to remove the HTML versions of the /more/ routes to simplify routing. The PR for this: https://github.com/guardian/frontend/pull/27939

However, we later realised that the DCAR implementation did not support a non-JavaScript experience for the More button. To fix this, we needed a way to render the More button as a fallback link (i.e. an anchor tag) that simply navigates to the next page of matches when JavaScript is disabled.

Since the `/more/` routes are being deprecated, we now use the equivalent non-/more/ route to construct that fallback link — this is provided via the new `nextPageNoJsUrl` prop.


## TOTO:
This PR can be merged after https://github.com/guardian/frontend/pull/27946


Diff:
diff --git a/dotcom-rendering/fixtures/manual/footballData.ts b/dotcom-rendering/fixtures/manual/footballData.ts
index 796dc2a1cd3..d8103fdb6da 100644
--- a/dotcom-rendering/fixtures/manual/footballData.ts
+++ b/dotcom-rendering/fixtures/manual/footballData.ts
@@ -231,3 +231,6 @@ export const moreDays: FootballMatches = [
 		],
 	},
 ];
+
+export const nextPageNoJsUrl =
+	'https://www.theguardian.com/football/fixtures/2025/May/05';
diff --git a/dotcom-rendering/src/components/FootballMatchList.stories.tsx b/dotcom-rendering/src/components/FootballMatchList.stories.tsx
index 45f0e6f3b44..2a1199635c3 100644
--- a/dotcom-rendering/src/components/FootballMatchList.stories.tsx
+++ b/dotcom-rendering/src/components/FootballMatchList.stories.tsx
@@ -1,7 +1,11 @@
 import type { Meta, StoryObj } from '@storybook/react';
-import { userEvent, within } from '@storybook/test';
+import { expect, userEvent, within } from '@storybook/test';
 import { allModes } from '../../.storybook/modes';
-import { initialDays, moreDays } from '../../fixtures/manual/footballData';
+import {
+	initialDays,
+	moreDays,
+	nextPageNoJsUrl,
+} from '../../fixtures/manual/footballData';
 import { error, ok } from '../lib/result';
 import { FootballMatchList } from './FootballMatchList';
 
@@ -39,13 +43,31 @@ export const Default = {
 		guardianBaseUrl: 'https://www.theguardian.com',
 		initialDays,
 		getMoreDays: () => Promise.resolve(ok(moreDays)),
+		nextPageNoJsUrl,
 	},
 	play: async ({ canvasElement }) => {
 		const canvas = within(canvasElement);
-		const moreButtons = canvas.getAllByRole('button');
+
+		// Get element with area role link (anchor tag) with the visible text content "More"
+		const moreLinks = canvas.getAllByRole('link', { name: /more/i });
+		// Assert the href exists
+		for (const moreLink of moreLinks) {
+			void expect(moreLink).toHaveAttribute('href', nextPageNoJsUrl);
+		}
+
+		const moreButtons = await canvas.findAllByRole('button', {
+			name: /more/i,
+		});
 		for (const moreButton of moreButtons) {
 			await userEvent.click(moreButton);
 		}
+
+		const rootDivs = canvasElement.querySelectorAll('[data-color-scheme]');
+		for (const rootDiv of rootDivs) {
+			const newDays = rootDiv.querySelectorAll('section');
+			// Assert that the number of sections has increased
+			void expect(newDays.length).toBeGreaterThan(initialDays.length);
+		}
 	},
 } satisfies Story;
 
@@ -54,7 +76,25 @@ export const ErrorGettingMore = {
 		...Default.args,
 		getMoreDays: () => Promise.resolve(error('failed')),
 	},
-	play: Default.play,
+	play: async ({ canvasElement }) => {
+		const canvas = within(canvasElement);
+
+		// Get the button element with the visible text content "More" and click it
+		const moreButtons = await canvas.findAllByRole('button', {
+			name: /more/i,
+		});
+		for (const moreButton of moreButtons) {
+			await userEvent.click(moreButton);
+		}
+
+		const rootDivs = canvasElement.querySelectorAll('[data-color-scheme]');
+		for (const rootDiv of rootDivs) {
+			// Assert that the error message appears
+			await within(rootDiv as HTMLElement).findByText(
+				/Could not get more matches\. Please try again later!/i,
+			);
+		}
+	},
 } satisfies Story;
 
 export const NoMoreDays = {
diff --git a/dotcom-rendering/src/components/FootballMatchList.tsx b/dotcom-rendering/src/components/FootballMatchList.tsx
index 09c2be40553..d28b6ee3fd3 100644
--- a/dotcom-rendering/src/components/FootballMatchList.tsx
+++ b/dotcom-rendering/src/components/FootballMatchList.tsx
@@ -11,6 +11,7 @@ import {
 import {
 	Button,
 	InlineError,
+	LinkButton,
 	SvgPlus,
 } from '@guardian/source/react-components';
 import { Fragment, type ReactNode, useState } from 'react';
@@ -26,6 +27,7 @@ import {
 	getTimeZoneFromEdition,
 } from '../lib/edition';
 import type { Result } from '../lib/result';
+import { useHydrated } from '../lib/useHydrated';
 import { palette } from '../palette';
 
 type Props = {
@@ -34,6 +36,7 @@ type Props = {
 	guardianBaseUrl: string;
 	getMoreDays?: () => Promise<Result<'failed', FootballMatches>>;
 	now: string;
+	nextPageNoJsUrl?: string;
 };
 
 const REMOVE_TRAILING_DOTS_REGEX = /\.+$/;
@@ -423,6 +426,7 @@ export const FootballMatchList = ({
 	guardianBaseUrl,
 	initialDays,
 	getMoreDays,
+	nextPageNoJsUrl,
 	now,
 }: Props) => {
 	const dateFormatter = new Intl.DateTimeFormat('en-GB', {
@@ -437,6 +441,8 @@ export const FootballMatchList = ({
 	const [days, setDays] = useState(initialDays);
 	const [isError, setIsError] = useState<boolean>(false);
 
+	const hydrated = useHydrated();
+
 	return (
 		<>
 			{days.map((day) => (
@@ -478,7 +484,8 @@ export const FootballMatchList = ({
 				</section>
 			))}
 
-			{getMoreDays === undefined ? null : (
+			{getMoreDays === undefined ||
+			nextPageNoJsUrl === undefined ? null : (
 				<div css={footballMatchesGridStyles}>
 					<div
 						css={css`
@@ -489,44 +496,70 @@ export const FootballMatchList = ({
 							}
 						`}
 					>
-						<Button
-							theme={{
-								textPrimary: palette('--button-text-primary'),
-								backgroundPrimary: palette(
-									'--button-background-primary',
-								),
-								backgroundPrimaryHover: palette(
-									'--button-background-primary-hover',
-								),
-							}}
-							icon={<SvgPlus />}
-							size="xsmall"
-							onClick={() => {
-								void getMoreDays().then((moreDays) => {
-									if (moreDays.kind === 'ok') {
-										setIsError(false);
-										setDays(days.concat(moreDays.value));
-									} else {
-										setIsError(true);
-									}
-								});
-							}}
-						>
-							More
-						</Button>
-						{isError ? (
-							<InlineError
-								cssOverrides={css`
-									padding-top: ${space[4]}px;
-									color: ${palette(
-										'--football-match-list-error',
-									)};
-								`}
+						{hydrated ? (
+							<>
+								<Button
+									theme={{
+										textPrimary: palette(
+											'--button-text-primary',
+										),
+										backgroundPrimary: palette(
+											'--button-background-primary',
+										),
+										backgroundPrimaryHover: palette(
+											'--button-background-primary-hover',
+										),
+									}}
+									icon={<SvgPlus />}
+									size="xsmall"
+									onClick={() => {
+										void getMoreDays().then((moreDays) => {
+											if (moreDays.kind === 'ok') {
+												setIsError(false);
+												setDays(
+													days.concat(moreDays.value),
+												);
+											} else {
+												setIsError(true);
+											}
+										});
+									}}
+								>
+									More
+								</Button>
+								{isError ? (
+									<InlineError
+										cssOverrides={css`
+											padding-top: ${space[4]}px;
+											color: ${palette(
+												'--football-match-list-error',
+											)};
+										`}
+									>
+										Could not get more matches. Please try
+										again later!
+									</InlineError>
+								) : null}
+							</>
+						) : (
+							<LinkButton
+								href={nextPageNoJsUrl}
+								size="xsmall"
+								theme={{
+									textPrimary: palette(
+										'--button-text-primary',
+									),
+									backgroundPrimary: palette(
+										'--button-background-primary',
+									),
+									backgroundPrimaryHover: palette(
+										'--button-background-primary-hover',
+									),
+								}}
 							>
-								Could not get more matches. Please try again
-								later!
-							</InlineError>
-						) : null}
+								More
+							</LinkButton>
+						)}
 					</div>
 				</div>
 			)}
diff --git a/dotcom-rendering/src/components/FootballMatchesPage.tsx b/dotcom-rendering/src/components/FootballMatchesPage.tsx
index 297cb0ccda0..be3fa78ea5b 100644
--- a/dotcom-rendering/src/components/FootballMatchesPage.tsx
+++ b/dotcom-rendering/src/components/FootballMatchesPage.tsx
@@ -21,6 +21,7 @@ type Props = {
 	kind: FootballMatchListPageKind;
 	initialDays: FootballMatches;
 	edition: EditionId;
+	nextPageNoJsUrl?: string;
 	goToCompetitionSpecificPage: (tag: string) => void;
 	getMoreDays?: () => Promise<Result<'failed', FootballMatches>>;
 	renderAds: boolean;
@@ -50,6 +51,7 @@ export const FootballMatchesPage = ({
 	kind,
 	initialDays,
 	edition,
+	nextPageNoJsUrl,
 	goToCompetitionSpecificPage,
 	getMoreDays,
 	renderAds,
@@ -125,6 +127,7 @@ export const FootballMatchesPage = ({
 				edition={edition}
 				getMoreDays={getMoreDays}
 				guardianBaseUrl={guardianBaseUrl}
+				nextPageNoJsUrl={nextPageNoJsUrl}
 			/>
 		</div>
 
diff --git a/dotcom-rendering/src/components/FootballMatchesPageWrapper.importable.tsx b/dotcom-rendering/src/components/FootballMatchesPageWrapper.importable.tsx
index 7500d40272f..6b4e5cdb283 100644
--- a/dotcom-rendering/src/components/FootballMatchesPageWrapper.importable.tsx
+++ b/dotcom-rendering/src/components/FootballMatchesPageWrapper.importable.tsx
@@ -68,6 +68,7 @@ type Props = {
 	kind: FootballMatchListPageKind;
 	initialDays: FootballMatches;
 	secondPage?: string;
+	nextPageNoJsUrl?: string;
 	edition: EditionId;
 	renderAds: boolean;
 	pageId: string;
@@ -81,6 +82,7 @@ export const FootballMatchesPageWrapper = ({
 	kind,
 	initialDays,
 	secondPage,
+	nextPageNoJsUrl,
 	edition,
 	renderAds,
 	pageId,
@@ -95,6 +97,7 @@ export const FootballMatchesPageWrapper = ({
 			initialDays={initialDays}
 			now={now}
 			edition={edition}
+			nextPageNoJsUrl={nextPageNoJsUrl}
 			goToCompetitionSpecificPage={goToCompetitionSpecificPage(
 				guardianBaseUrl,
 			)}
diff --git a/dotcom-rendering/src/frontend/feFootballMatchListPage.ts b/dotcom-rendering/src/frontend/feFootballMatchListPage.ts
index 29b732d002b..2f47715dfb6 100644
--- a/dotcom-rendering/src/frontend/feFootballMatchListPage.ts
+++ b/dotcom-rendering/src/frontend/feFootballMatchListPage.ts
@@ -93,5 +93,6 @@ export type FEFootballMatchListPage = FEFootballDataPage & {
 	filters: Record<string, FEFootballCompetition[]>;
 	matchesList: FEMatchByDateAndCompetition[];
 	nextPage?: string;
+	nextPageNoJs?: string;
 	previousPage?: string;
 };
diff --git a/dotcom-rendering/src/frontend/schemas/feFootballMatchListPage.json b/dotcom-rendering/src/frontend/schemas/feFootballMatchListPage.json
index 0707ef78256..53779858dd6 100644
--- a/dotcom-rendering/src/frontend/schemas/feFootballMatchListPage.json
+++ b/dotcom-rendering/src/frontend/schemas/feFootballMatchListPage.json
@@ -114,6 +114,9 @@
                 "nextPage": {
                     "type": "string"
                 },
+                "nextPageNoJs": {
+                    "type": "string"
+                },
                 "previousPage": {
                     "type": "string"
                 }
diff --git a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx
index ff99733b68a..2b1a37fdb42 100644
--- a/dotcom-rendering/src/layouts/SportDataPageLayout.tsx
+++ b/dotcom-rendering/src/layouts/SportDataPageLayout.tsx
@@ -40,6 +40,7 @@ const SportsPage = ({
 						kind={sportData.kind}
 						initialDays={sportData.matchesList}
 						secondPage={sportData.nextPage}
+						nextPageNoJsUrl={sportData.nextPageNoJsUrl}
 						edition={sportData.editionId}
 						renderAds={renderAds}
 						pageId={sportData.config.pageId}
diff --git a/dotcom-rendering/src/server/handler.sportDataPage.web.ts b/dotcom-rendering/src/server/handler.sportDataPage.web.ts
index 69b97958967..db0252fe03b 100644
--- a/dotcom-rendering/src/server/handler.sportDataPage.web.ts
+++ b/dotcom-rendering/src/server/handler.sportDataPage.web.ts
@@ -75,6 +75,14 @@ const parseFEFootballCompetitionRegions = (
 		.sort(sortRegionsFunction);
 };
 
+const getNextPageNoJsUrl = (isProd: boolean, nextPageNoJs?: string) => {
+	if (!nextPageNoJs) {
+		return undefined;
+	}
+	if (isProd) return `https://www.theguardian.com${nextPageNoJs}`;
+	return `https://code.dev-theguardian.com${nextPageNoJs}`;
+};
+
 const parseFEFootballMatchList = (
 	data: FEFootballMatchListPage,
 ): FootballMatchListPage => {
@@ -93,6 +101,10 @@ const parseFEFootballMatchList = (
 		now: new Date().toISOString(),
 		kind: decideMatchListPageKind(data.config.pageId),
 		nextPage: data.nextPage,
+		nextPageNoJsUrl: getNextPageNoJsUrl(
+			data.config.isProd,
+			data.nextPageNoJs,
+		),
 		previousPage: data.previousPage,
 		regions: parseFEFootballCompetitionRegions(data.filters),
 		nav: {
diff --git a/dotcom-rendering/src/sportDataPage.ts b/dotcom-rendering/src/sportDataPage.ts
index 2e273262038..4bab118adc4 100644
--- a/dotcom-rendering/src/sportDataPage.ts
+++ b/dotcom-rendering/src/sportDataPage.ts
@@ -29,6 +29,7 @@ export type SportPageConfig = {
 
 export type FootballMatchListPage = FootballData & {
 	nextPage?: string;
+	nextPageNoJsUrl?: string;
 	previousPage?: string;
 	matchesList: FootballMatches;
 	now: string;


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-02T14:01:12Z
user: login: SiAdcock
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:52:35Z
user: login: marjisound
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T09:04:56Z
user: login: SiAdcock
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T10:39:46Z
user: login: marjisound
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T10:57:57Z
user: login: JamieB-gu
body: Looks good! Couple of questions.

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T13:17:52Z
user: login: marjisound
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T08:54:59Z
user: login: marjisound
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T08:55:17Z
user: login: SiAdcock
body: Wonderful, thanks @marjisound 🙏 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T11:02:10Z

Review comments:
diff_hunk: @@ -478,16 +480,18 @@
 				</section>
 			))}
 
-			{getMoreDays === undefined ? null : (
+			{getMoreDays === undefined ||
+			nextPageNoJsUrl === undefined ? null : (
 				<div css={footballMatchesGridStyles}>
-					<div
+					<a
 						css={css`
 							grid-column: centre-column-start / centre-column-end;
 
 							${until.leftCol} {
 								padding-top: ${space[10]}px;
 							}
 						`}
+						href={nextPageNoJsUrl}
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: github-advanced-security[bot]
body: ## Client-side cross-site scripting

Cross-site scripting vulnerability due to [user-provided value](1).

[Show more details](https://github.com/guardian/dotcom-rendering/security/code-scanning/567)
created_at: 2025-05-02T14:01:12Z
author_association: NONE
diff_hunk: @@ -478,16 +480,18 @@
 				</section>
 			))}
 
-			{getMoreDays === undefined ? null : (
+			{getMoreDays === undefined ||
+			nextPageNoJsUrl === undefined ? null : (
 				<div css={footballMatchesGridStyles}>
-					<div
+					<a
 						css={css`
 							grid-column: centre-column-start / centre-column-end;
 
 							${until.leftCol} {
 								padding-top: ${space[10]}px;
 							}
 						`}
+						href={nextPageNoJsUrl}
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: github-advanced-security[bot]
body: ## Client-side URL redirect

Untrusted URL redirection depends on a [user-provided value](1).

[Show more details](https://github.com/guardian/dotcom-rendering/security/code-scanning/568)
created_at: 2025-05-02T14:01:12Z
author_association: NONE
diff_hunk: @@ -478,16 +480,18 @@ export const FootballMatchList = ({
 				</section>
 			))}
 
-			{getMoreDays === undefined ? null : (
+			{getMoreDays === undefined ||
+			nextPageNoJsUrl === undefined ? null : (
 				<div css={footballMatchesGridStyles}>
-					<div
+					<a
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: SiAdcock
body: I think this is minor, raising out of interest. Nesting a button inside a link is [not valid HTML](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element):

> [Content model](https://html.spec.whatwg.org/multipage/dom.html#concept-element-content-model):
[Transparent](https://html.spec.whatwg.org/multipage/dom.html#transparent), but there must be no [interactive content](https://html.spec.whatwg.org/multipage/dom.html#interactive-content-2) descendant, [a](https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-a-element) element descendant, or descendant with the [tabindex](https://html.spec.whatwg.org/multipage/interaction.html#attr-tabindex) attribute specified.

There might be [weird behaviour](https://theodorusclarence.com/blog/nesting-button-inside-link#putting-icons-inside-a-button) as a result. 

An alternative solution might be to retain the outer `<div>` and only hide the `<a>` if JS loads. I haven't tested this.

```tsx
<div>
  {
    jsLoaded ? 
    (<Button></Button>)
    :
    (<LinkButton></LinkButton>)
  }
</div>
```
created_at: 2025-05-08T16:52:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -478,16 +480,18 @@ export const FootballMatchList = ({
 				</section>
 			))}
 
-			{getMoreDays === undefined ? null : (
+			{getMoreDays === undefined ||
+			nextPageNoJsUrl === undefined ? null : (
 				<div css={footballMatchesGridStyles}>
-					<div
+					<a
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: marjisound
body: Thanks @SiAdcock  for pointing this out and sharing the links. I made some changes and used a state `jsEnabled` which is set within a `useEffect`. Using that the component decides if it needs to use the link or the button. What do you think about this approach?
created_at: 2025-05-12T09:04:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -437,6 +440,13 @@ export const FootballMatchList = ({
 	const [days, setDays] = useState(initialDays);
 	const [isError, setIsError] = useState<boolean>(false);
 
+	const [jsLoaded, setJsLoaded] = useState(false);
+
+	useEffect(() => {
+		// This will only run after JS has loaded on the client
+		setJsLoaded(true);
+	}, []);
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: SiAdcock
body: So sorry @marjisound, I've just noticed we have a [`useHydrated` hook](https://github.com/guardian/dotcom-rendering/blame/main/dotcom-rendering/src/lib/useHydrated.ts) that does the same thing. Would you be able to use it here?
created_at: 2025-05-12T10:39:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -437,6 +440,13 @@ export const FootballMatchList = ({
 	const [days, setDays] = useState(initialDays);
 	const [isError, setIsError] = useState<boolean>(false);
 
+	const [jsLoaded, setJsLoaded] = useState(false);
+
+	useEffect(() => {
+		// This will only run after JS has loaded on the client
+		setJsLoaded(true);
+	}, []);
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: marjisound
body: Ah thanks for letting me know, just changed it to use the `useHydrated` hook 
created_at: 2025-05-12T10:57:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -231,3 +231,6 @@ export const moreDays: FootballMatches = [
 		],
 	},
 ];
+
+export const nextPageNoJsUrl =
+	'https://www.theguardian.com/football/fixtures/more/2025/May/05';
path: dotcom-rendering/fixtures/manual/footballData.ts
user: login: JamieB-gu
body: Aren't the routes with `/more` in the path being deprecated?

created_at: 2025-05-12T13:04:38Z
author_association: CONTRIBUTOR
path: dotcom-rendering/src/components/FootballMatchList.stories.tsx
user: login: JamieB-gu
body: 👍 for these storybook tests!

created_at: 2025-05-12T13:07:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,44 +496,71 @@ export const FootballMatchList = ({
 							}
 						`}
 					>
-						<Button
-							theme={{
-								textPrimary: palette('--button-text-primary'),
-								backgroundPrimary: palette(
-									'--button-background-primary',
-								),
-								backgroundPrimaryHover: palette(
-									'--button-background-primary-hover',
-								),
-							}}
-							icon={<SvgPlus />}
-							size="xsmall"
-							onClick={() => {
-								void getMoreDays().then((moreDays) => {
-									if (moreDays.kind === 'ok') {
-										setIsError(false);
-										setDays(days.concat(moreDays.value));
-									} else {
-										setIsError(true);
-									}
-								});
-							}}
-						>
-							More
-						</Button>
-						{isError ? (
-							<InlineError
-								cssOverrides={css`
-									padding-top: ${space[4]}px;
-									color: ${palette(
-										'--football-match-list-error',
-									)};
-								`}
+						{hydrated ? (
+							<>
+								<Button
+									theme={{
+										textPrimary: palette(
+											'--button-text-primary',
+										),
+										backgroundPrimary: palette(
+											'--button-background-primary',
+										),
+										backgroundPrimaryHover: palette(
+											'--button-background-primary-hover',
+										),
+									}}
+									icon={<SvgPlus />}
+									size="xsmall"
+									onClick={(e) => {
+										e.preventDefault(); // prevent navigation when JS is enabled
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: JamieB-gu
body: If there's no link wrapping the button now is this still needed?

created_at: 2025-05-12T13:11:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -231,3 +231,6 @@ export const moreDays: FootballMatches = [
 		],
 	},
 ];
+
+export const nextPageNoJsUrl =
+	'https://www.theguardian.com/football/fixtures/more/2025/May/05';
path: dotcom-rendering/fixtures/manual/footballData.ts
user: login: marjisound
body: Good point, I removed the more from the url
created_at: 2025-05-13T08:54:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -489,44 +496,71 @@ export const FootballMatchList = ({
 							}
 						`}
 					>
-						<Button
-							theme={{
-								textPrimary: palette('--button-text-primary'),
-								backgroundPrimary: palette(
-									'--button-background-primary',
-								),
-								backgroundPrimaryHover: palette(
-									'--button-background-primary-hover',
-								),
-							}}
-							icon={<SvgPlus />}
-							size="xsmall"
-							onClick={() => {
-								void getMoreDays().then((moreDays) => {
-									if (moreDays.kind === 'ok') {
-										setIsError(false);
-										setDays(days.concat(moreDays.value));
-									} else {
-										setIsError(true);
-									}
-								});
-							}}
-						>
-							More
-						</Button>
-						{isError ? (
-							<InlineError
-								cssOverrides={css`
-									padding-top: ${space[4]}px;
-									color: ${palette(
-										'--football-match-list-error',
-									)};
-								`}
+						{hydrated ? (
+							<>
+								<Button
+									theme={{
+										textPrimary: palette(
+											'--button-text-primary',
+										),
+										backgroundPrimary: palette(
+											'--button-background-primary',
+										),
+										backgroundPrimaryHover: palette(
+											'--button-background-primary-hover',
+										),
+									}}
+									icon={<SvgPlus />}
+									size="xsmall"
+									onClick={(e) => {
+										e.preventDefault(); // prevent navigation when JS is enabled
path: dotcom-rendering/src/components/FootballMatchList.tsx
user: login: marjisound
body: You're right. I removed it 👍 
created_at: 2025-05-13T08:55:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-02T14:01:47Z
body: **Size Change:** +210 B (+0.02%) 

**Total Size:** 973 kB

| Filename | Size | Change |  |
| :--- | :---: | :---: | :---: |
| `dotcom-rendering/dist/2345.client.web.********************.js` | 0 B | -3.38 kB (removed) | 🏆 |
| `dotcom-rendering/dist/FootballMatchesPageWrapper-importable.client.web.********************.js` | 7.31 kB | +115 B (+1.6%) |  |
| `dotcom-rendering/dist/5715.client.web.********************.js` | 3.48 kB | +3.48 kB (new file) | 🆕 |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change |
| :--- | :---: | :---: |
| `dotcom-rendering/dist/1008.client.web.********************.js` | 2.88 kB | 0 B |
| `dotcom-rendering/dist/1032.client.web.********************.js` | 3.31 kB | 0 B |
| `dotcom-rendering/dist/117.client.web.********************.js` | 2.87 kB | 0 B |
| `dotcom-rendering/dist/1562.client.web.********************.js` | 20.3 kB | 0 B |
| `dotcom-rendering/dist/1656.client.web.********************.js` | 49.6 kB | 0 B |
| `dotcom-rendering/dist/1823.client.web.********************.js` | 3.9 kB | 0 B |
| `dotcom-rendering/dist/2045.client.web.********************.js` | 2.63 kB | 0 B |
| `dotcom-rendering/dist/2161.client.web.********************.js` | 3.88 kB | 0 B |
| `dotcom-rendering/dist/223.client.web.********************.js` | 527 B | 0 B |
| `dotcom-rendering/dist/2270.client.web.********************.js` | 3.65 kB | 0 B |
| `dotcom-rendering/dist/2309.client.web.********************.js` | 3.27 kB | 0 B |
| `dotcom-rendering/dist/2360.client.web.********************.js` | 20 kB | 0 B |
| `dotcom-rendering/dist/2502.client.web.********************.js` | 157 B | 0 B |
| `dotcom-rendering/dist/302.client.web.********************.js` | 8.84 kB | 0 B |
| `dotcom-rendering/dist/3226.client.web.********************.js` | 2.85 kB | 0 B |
| `dotcom-rendering/dist/3292.client.web.********************.js` | 2.11 kB | 0 B |
| `dotcom-rendering/dist/3308.client.web.********************.js` | 3.12 kB | 0 B |
| `dotcom-rendering/dist/3485.client.web.********************.js` | 4.67 kB | 0 B |
| `dotcom-rendering/dist/3598.client.web.********************.js` | 3.65 kB | 0 B |
| `dotcom-rendering/dist/3605.client.web.********************.js` | 15.1 kB | 0 B |
| `dotcom-rendering/dist/3619.client.web.********************.js` | 4.46 kB | 0 B |
| `dotcom-rendering/dist/3950.client.web.********************.js` | 3.36 kB | 0 B |
| `dotcom-rendering/dist/4133.client.web.********************.js` | 3.1 kB | 0 B |
| `dotcom-rendering/dist/4258.client.web.********************.js` | 4.08 kB | 0 B |
| `dotcom-rendering/dist/4306.client.web.********************.js` | 22.7 kB | 0 B |
| `dotcom-rendering/dist/4308.client.web.********************.js` | 3.57 kB | 0 B |
| `dotcom-rendering/dist/4324.client.web.********************.js` | 619 B | 0 B |
| `dotcom-rendering/dist/4457.client.web.********************.js` | 6.26 kB | 0 B |
| `dotcom-rendering/dist/4501.client.web.********************.js` | 4.29 kB | 0 B |
| `dotcom-rendering/dist/476.client.web.********************.js` | 5.42 kB | 0 B |
| `dotcom-rendering/dist/5144.client.web.********************.js` | 2.55 kB | 0 B |
| `dotcom-rendering/dist/5448.client.web.********************.js` | 4.37 kB | 0 B |
| `dotcom-rendering/dist/545.client.web.********************.js` | 531 B | 0 B |
| `dotcom-rendering/dist/5455.client.web.********************.js` | 7.15 kB | 0 B |
| `dotcom-rendering/dist/562.client.web.********************.js` | 11.5 kB | 0 B |
| `dotcom-rendering/dist/6038.client.web.********************.js` | 2.76 kB | 0 B |
| `dotcom-rendering/dist/6217.client.web.********************.js` | 3.75 kB | 0 B |
| `dotcom-rendering/dist/6480.client.web.********************.js` | 3.68 kB | 0 B |
| `dotcom-rendering/dist/6540.client.web.********************.js` | 2.62 kB | 0 B |
| `dotcom-rendering/dist/6541.client.web.********************.js` | 2.91 kB | 0 B |
| `dotcom-rendering/dist/6562.client.web.********************.js` | 439 B | 0 B |
| `dotcom-rendering/dist/6584.client.web.********************.js` | 2.68 kB | 0 B |
| `dotcom-rendering/dist/6722.client.web.********************.js` | 3.03 kB | 0 B |
| `dotcom-rendering/dist/6802.client.web.********************.js` | 2.51 kB | 0 B |
| `dotcom-rendering/dist/6873.client.web.********************.js` | 4.13 kB | 0 B |
| `dotcom-rendering/dist/695.client.web.********************.js` | 4.51 kB | 0 B |
| `dotcom-rendering/dist/7116.client.web.********************.js` | 23.2 kB | 0 B |
| `dotcom-rendering/dist/732.client.web.********************.js` | 3.07 kB | 0 B |
| `dotcom-rendering/dist/7398.client.web.********************.js` | 3.57 kB | 0 B |
| `dotcom-rendering/dist/7487.client.web.********************.js` | 4.25 kB | 0 B |
| `dotcom-rendering/dist/7927.client.web.********************.js` | 2.76 kB | 0 B |
| `dotcom-rendering/dist/8151.client.web.********************.js` | 4.17 kB | 0 B |
| `dotcom-rendering/dist/8212.client.web.********************.js` | 5.04 kB | 0 B |
| `dotcom-rendering/dist/8385.client.web.********************.js` | 3.88 kB | 0 B |
| `dotcom-rendering/dist/8387.client.web.********************.js` | 2.78 kB | 0 B |
| `dotcom-rendering/dist/8436.client.web.********************.js` | 3.63 kB | 0 B |
| `dotcom-rendering/dist/8602.client.web.********************.js` | 3.39 kB | 0 B |
| `dotcom-rendering/dist/8619.client.web.********************.js` | 4.44 kB | 0 B |
| `dotcom-rendering/dist/864.client.web.********************.js` | 3.17 kB | 0 B |
| `dotcom-rendering/dist/8942.client.web.********************.js` | 4.44 kB | 0 B |
| `dotcom-rendering/dist/8973.client.web.********************.js` | 16.5 kB | 0 B |
| `dotcom-rendering/dist/8985.client.web.********************.js` | 11.1 kB | 0 B |
| `dotcom-rendering/dist/9018.client.web.********************.js` | 3.73 kB | 0 B |
| `dotcom-rendering/dist/9135.client.web.********************.js` | 3.54 kB | 0 B |
| `dotcom-rendering/dist/9139.client.web.********************.js` | 3.13 kB | 0 B |
| `dotcom-rendering/dist/9376.client.web.********************.js` | 2.52 kB | 0 B |
| `dotcom-rendering/dist/960.client.web.********************.js` | 7.99 kB | 0 B |
| `dotcom-rendering/dist/967.client.web.********************.js` | 2.87 kB | 0 B |
| `dotcom-rendering/dist/9709.client.web.********************.js` | 2.73 kB | 0 B |
| `dotcom-rendering/dist/9822.client.web.********************.js` | 7.63 kB | 0 B |
| `dotcom-rendering/dist/9837.client.web.********************.js` | 3.29 kB | 0 B |
| `dotcom-rendering/dist/9870.client.web.********************.js` | 4.77 kB | 0 B |
| `dotcom-rendering/dist/Accessibility-importable.client.web.********************.js` | 8.43 kB | 0 B |
| `dotcom-rendering/dist/AdBlockAsk-importable.client.web.********************.js` | 2.99 kB | 0 B |
| `dotcom-rendering/dist/AdPortals-importable.client.web.********************.js` | 4.76 kB | 0 B |
| `dotcom-rendering/dist/AlreadyVisited-importable.client.web.********************.js` | 425 B | 0 B |
| `dotcom-rendering/dist/AppsEpic-importable.client.web.********************.js` | 3.61 kB | 0 B |
| `dotcom-rendering/dist/AppsFooter-importable.client.web.********************.js` | 2.69 kB | 0 B |
| `dotcom-rendering/dist/AppsLightboxImage-importable.client.web.********************.js` | 2.66 kB | 0 B |
| `dotcom-rendering/dist/AppsLightboxImageStore-importable.client.web.********************.js` | 2.6 kB | 0 B |
| `dotcom-rendering/dist/AudioAtomWrapper-importable.client.web.********************.js` | 2.76 kB | 0 B |
| `dotcom-rendering/dist/AudioPlayerWrapper-importable.client.web.********************.js` | 6.61 kB | 0 B |
| `dotcom-rendering/dist/AustralianTerritorySwitcher-importable.client.web.********************.js` | 4.61 kB | 0 B |
| `dotcom-rendering/dist/Branding-importable.client.web.********************.js` | 2.89 kB | 0 B |
| `dotcom-rendering/dist/braze-web-sdk-core.client.web.********************.js` | 67.7 kB | 0 B |
| `dotcom-rendering/dist/BrazeMessaging-importable.client.web.********************.js` | 1.68 kB | 0 B |
| `dotcom-rendering/dist/CalloutBlockComponent-importable.client.web.********************.js` | 6.73 kB | 0 B |
| `dotcom-rendering/dist/CalloutEmbedBlockComponent-importable.client.web.********************.js` | 5.77 kB | 0 B |
| `dotcom-rendering/dist/CardCommentCount-importable.client.web.********************.js` | 2.67 kB | 0 B |
| `dotcom-rendering/dist/Carousel-importable.client.web.********************.js` | 6.67 kB | 0 B |
| `dotcom-rendering/dist/CarouselForNewsletters-importable.client.web.********************.js` | 4.55 kB | 0 B |
| `dotcom-rendering/dist/ChartAtom-importable.client.web.********************.js` | 539 B | 0 B |
| `dotcom-rendering/dist/CommentCount-importable.client.web.********************.js` | 2.3 kB | 0 B |
| `dotcom-rendering/dist/CrosswordComponent-importable.client.web.********************.js` | 2.88 kB | 0 B |
| `dotcom-rendering/dist/DiscussionApps-importable.client.web.********************.js` | 1.07 kB | 0 B |
| `dotcom-rendering/dist/DiscussionMeta-importable.client.web.********************.js` | 2.41 kB | 0 B |
| `dotcom-rendering/dist/DiscussionWeb-importable.client.web.********************.js` | 3.45 kB | 0 B |
| `dotcom-rendering/dist/DocumentBlockComponent-importable.client.web.********************.js` | 2.85 kB | 0 B |
| `dotcom-rendering/dist/Dropdown-importable.client.web.********************.js` | 1.72 kB | 0 B |
| `dotcom-rendering/dist/EditionSwitcherBanner-importable.client.web.********************.js` | 4.43 kB | 0 B |
| `dotcom-rendering/dist/EmbedBlockComponent-importable.client.web.********************.js` | 3.98 kB | 0 B |
| `dotcom-rendering/dist/EnhancePinnedPost-importable.client.web.********************.js` | 2.02 kB | 0 B |
| `dotcom-rendering/dist/FetchOnwardsData-importable.client.web.********************.js` | 1.94 kB | 0 B |
| `dotcom-rendering/dist/FilterKeyEventsToggle-importable.client.web.********************.js` | 3.72 kB | 0 B |
| `dotcom-rendering/dist/FocusStyles-importable.client.web.********************.js` | 618 B | 0 B |
| `dotcom-rendering/dist/FollowWrapper-importable.client.web.********************.js` | 2.53 kB | 0 B |
| `dotcom-rendering/dist/FootballTablesCompetitionSelect-importable.client.web.********************.js` | 3.26 kB | 0 B |
| `dotcom-rendering/dist/FooterLabel-importable.client.web.********************.js` | 364 B | 0 B |
| `dotcom-rendering/dist/FooterReaderRevenueLinks-importable.client.web.********************.js` | 3.43 kB | 0 B |
| `dotcom-rendering/dist/frameworks.client.web.********************.js` | 20.9 kB | 0 B |
| `dotcom-rendering/dist/FrontSubNav-importable.client.web.********************.js` | 7.5 kB | 0 B |
| `dotcom-rendering/dist/GetCricketScoreboard-importable.client.web.********************.js` | 6.29 kB | 0 B |
| `dotcom-rendering/dist/GetMatchNav-importable.client.web.********************.js` | 11.5 kB | 0 B |
| `dotcom-rendering/dist/GetMatchStats-importable.client.web.********************.js` | 8.1 kB | 0 B |
| `dotcom-rendering/dist/GetMatchTabs-importable.client.web.********************.js` | 2.58 kB | 0 B |
| `dotcom-rendering/dist/guardian-braze-components-banner.client.web.********************.js` | 16.1 kB | 0 B |
| `dotcom-rendering/dist/guardian-braze-components-end-of-article.client.web.********************.js` | 10.2 kB | 0 B |
| `dotcom-rendering/dist/GuideAtomWrapper-importable.client.web.********************.js` | 782 B | 0 B |
| `dotcom-rendering/dist/index.client.web.********************.js` | 46.4 kB | -1 B (0%) |
| `dotcom-rendering/dist/InstagramBlockComponent-importable.client.web.********************.js` | 2.9 kB | 0 B |
| `dotcom-rendering/dist/InteractiveAtomMessenger-importable.client.web.********************.js` | 854 B | 0 B |
| `dotcom-rendering/dist/InteractiveBlockComponent-importable.client.web.********************.js` | 8.83 kB | 0 B |
| `dotcom-rendering/dist/InteractiveContentsBlockComponent-importable.client.web.********************.js` | 3.75 kB | 0 B |
| `dotcom-rendering/dist/KeyEventsCarousel-importable.client.web.********************.js` | 5.69 kB | 0 B |
| `dotcom-rendering/dist/KnowledgeQuizAtom-importable.client.web.********************.js` | 3.19 kB | 0 B |
| `dotcom-rendering/dist/LatestLinks-importable.client.web.********************.js` | 7.96 kB | 0 B |
| `dotcom-rendering/dist/LightboxHash-importable.client.web.********************.js` | 436 B | 0 B |
| `dotcom-rendering/dist/LightboxLayout-importable.client.web.********************.js` | 6.56 kB | 0 B |
| `dotcom-rendering/dist/LiveBlogEpic-importable.client.web.********************.js` | 3.56 kB | 0 B |
| `dotcom-rendering/dist/LiveblogGutterAskWrapper-importable.client.web.********************.js` | 2.49 kB | 0 B |
| `dotcom-rendering/dist/LiveblogNotifications-importable.client.web.********************.js` | 4.84 kB | 0 B |
| `dotcom-rendering/dist/Liveness-importable.client.web.********************.js` | 4.71 kB | 0 B |
| `dotcom-rendering/dist/LoopVideo-importable.client.web.********************.js` | 3.54 kB | 0 B |
| `dotcom-rendering/dist/ManyNewsletterSignUp-importable.client.web.********************.js` | 7.65 kB | 0 B |
| `dotcom-rendering/dist/MapEmbedBlockComponent-importable.client.web.********************.js` | 6.04 kB | 0 B |
| `dotcom-rendering/dist/Metrics-importable.client.web.********************.js` | 2.7 kB | 0 B |
| `dotcom-rendering/dist/MostViewedFooter-importable.client.web.********************.js` | 3.85 kB | 0 B |
| `dotcom-rendering/dist/MostViewedFooterData-importable.client.web.********************.js` | 5.97 kB | 0 B |
| `dotcom-rendering/dist/MostViewedRightWithAd-importable.client.web.********************.js` | 5.24 kB | 0 B |
| `dotcom-rendering/dist/OnwardsUpper-importable.client.web.********************.js` | 5.32 kB | 0 B |
| `dotcom-rendering/dist/PersonalityQuizAtom-importable.client.web.********************.js` | 3.35 kB | 0 B |
| `dotcom-rendering/dist/ProfileAtom-importable.client.web.********************.js` | 545 B | 0 B |
| `dotcom-rendering/dist/ProfileAtomWrapper-importable.client.web.********************.js` | 805 B | 0 B |
| `dotcom-rendering/dist/PulsingDot-importable.client.web.********************.js` | 749 B | 0 B |
| `dotcom-rendering/dist/QandaAtom-importable.client.web.********************.js` | 542 B | 0 B |
| `dotcom-rendering/dist/ReaderRevenueDev-importable.client.web.********************.js` | 470 B | 0 B |
| `dotcom-rendering/dist/readerRevenueDevUtils.client.web.********************.js` | 1.7 kB | 0 B |
| `dotcom-rendering/dist/RelativeTime-importable.client.web.********************.js` | 2.56 kB | 0 B |
| `dotcom-rendering/dist/RichLinkComponent-importable.client.web.********************.js` | 6.14 kB | 0 B |
| `dotcom-rendering/dist/ScrollableFeature-importable.client.web.********************.js` | 6.63 kB | 0 B |
| `dotcom-rendering/dist/ScrollableHighlights-importable.client.web.********************.js` | 7.26 kB | 0 B |
| `dotcom-rendering/dist/ScrollableMedium-importable.client.web.********************.js` | 2.11 kB | 0 B |
| `dotcom-rendering/dist/ScrollableSmall-importable.client.web.********************.js` | 2.17 kB | 0 B |
| `dotcom-rendering/dist/SecureSignup-importable.client.web.********************.js` | 4.2 kB | 0 B |
| `dotcom-rendering/dist/SendTargetingParams-importable.client.web.********************.js` | 2.21 kB | 0 B |
| `dotcom-rendering/dist/sentry.client.web.********************.js` | 802 B | 0 B |
| `dotcom-rendering/dist/SetABTests-importable.client.web.********************.js` | 3.91 kB | 0 B |
| `dotcom-rendering/dist/SetAdTargeting-importable.client.web.********************.js` | 487 B | 0 B |
| `dotcom-rendering/dist/ShareButton-importable.client.web.********************.js` | 2.18 kB | 0 B |
| `dotcom-rendering/dist/shimport.client.web.********************.js` | 2.8 kB | 0 B |
| `dotcom-rendering/dist/ShowHideContainers-importable.client.web.********************.js` | 893 B | 0 B |
| `dotcom-rendering/dist/ShowMore-importable.client.web.********************.js` | 919 B | 0 B |
| `dotcom-rendering/dist/SignInGateMain.client.web.********************.js` | 4.35 kB | 0 B |
| `dotcom-rendering/dist/SignInGateMainCheckoutComplete.client.web.********************.js` | 5.46 kB | 0 B |
| `dotcom-rendering/dist/SignInGateSelector-importable.client.web.********************.js` | 6.67 kB | 0 B |
| `dotcom-rendering/dist/SlideshowCarousel-importable.client.web.********************.js` | 4.56 kB | 0 B |
| `dotcom-rendering/dist/SlotBodyEnd-importable.client.web.********************.js` | 4.91 kB | 0 B |
| `dotcom-rendering/dist/SpotifyBlockComponent-importable.client.web.********************.js` | 5.8 kB | 0 B |
| `dotcom-rendering/dist/StickyBottomBanner-importable.client.web.********************.js` | 4.04 kB | 0 B |
| `dotcom-rendering/dist/SubNav-importable.client.web.********************.js` | 2.44 kB | 0 B |
| `dotcom-rendering/dist/TableOfContents-importable.client.web.********************.js` | 3.52 kB | 0 B |
| `dotcom-rendering/dist/TimelineAtom-importable.client.web.********************.js` | 1.23 kB | 0 B |
| `dotcom-rendering/dist/Titlepiece-importable.client.web.********************.js` | 13.6 kB | 0 B |
| `dotcom-rendering/dist/TopBar-importable.client.web.********************.js` | 8.85 kB | 0 B |
| `dotcom-rendering/dist/TopBarSupport-importable.client.web.********************.js` | 2.49 kB | 0 B |
| `dotcom-rendering/dist/TweetBlockComponent-importable.client.web.********************.js` | 1.13 kB | 0 B |
| `dotcom-rendering/dist/UnsafeEmbedBlockComponent-importable.client.web.********************.js` | 2.92 kB | 0 B |
| `dotcom-rendering/dist/VideoFacebookBlockComponent-importable.client.web.********************.js` | 6.06 kB | 0 B |
| `dotcom-rendering/dist/VineBlockComponent-importable.client.web.********************.js` | 3.32 kB | 0 B |
| `dotcom-rendering/dist/YoutubeBlockComponent-importable.client.web.********************.js` | 843 B | 0 B |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T09:01:50Z
body: Hello :wave:! When you're ready to run Chromatic, please apply the `run_chromatic` label to this PR.

You will need to reapply the label each time you want to run Chromatic.

[Click here to see the Chromatic project.](https://www.chromatic.com/builds?appId=63e251470cfbe61776b0ef19)
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T09:03:13Z
body: ### [Deploy build 42627 of `Mobile::mobile-apps-rendering` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=Mobile%3A%3Amobile-apps-rendering&build=42627&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
<details>
<summary>All deployment options</summary>

- [Deploy build 42627 of `Mobile::mobile-apps-rendering` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=Mobile%3A%3Amobile-apps-rendering&build=42627&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 42627 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=Mobile%3A%3Amobile-apps-rendering&build=42627&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=Mobile%3A%3Amobile-apps-rendering&stage=CODE)
</details>

---
_From [guardian/actions-riff-raff](https://github.com/guardian/actions-riff-raff)._
<!-- guardian/actions-riff-raff for Mobile::mobile-apps-rendering -->
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T10:14:03Z
body: ### [Deploy build 18421 of `dotcom:rendering-all` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=dotcom%3Arendering-all&build=18421&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
<details>
<summary>All deployment options</summary>

- [Deploy build 18421 of `dotcom:rendering-all` to CODE](https://riffraff.gutools.co.uk/deployment/deployAgain?project=dotcom%3Arendering-all&build=18421&stage=CODE&updateStrategy=MostlyHarmless&action=deploy)
- [Deploy parts of build 18421 to CODE by previewing it first](https://riffraff.gutools.co.uk/preview/yaml?project=dotcom%3Arendering-all&build=18421&stage=CODE&updateStrategy=MostlyHarmless)
- [What's on CODE right now?](https://riffraff.gutools.co.uk/deployment/history?projectName=dotcom%3Arendering-all&stage=CODE)
</details>

---
_From [guardian/actions-riff-raff](https://github.com/guardian/actions-riff-raff)._
<!-- guardian/actions-riff-raff for dotcom:rendering-all -->
author_association: NONE
user: login: prout-bot
created_at: 2025-05-19T08:38:11Z
body: Seen on [PROD](https://www.theguardian.com/uk) (merged by @marjisound 9 minutes and 56 seconds ago) Please check your changes!
author_association: NONE