(
+ ///
+ // `https://api.nextgen.guardianapps.co.uk/football/${footballCompetitionData.competitionId}/embed
+ // https://www.theguardian.com/football/world-cup-2026/live.json?dcr=true
+ // production use of this component would likely require using the live endpoint, but this will only return data if there's an actual match on the day
+ // for testing purposes, we can hardcode a match day endpoint to return data for a specific date, e.g. 2026-07-09, which provides at least one result
+ // 'https://api.nextgen.guardianapps.co.uk/football/match-day/2026/jul/09.json?dcr=true',
+ `https://api.nextgen.guardianapps.co.uk/football/${competitionName}/live.json?dcr=true`,
+ fetcher,
+ );
+
+ if (error) {
+ return Failed to load football data
;
+ }
+
+ if (data === undefined) {
+ return Loading…
;
+ }
+
+ const validated = safeParse(matchDayResponseSchema, data);
+ if (!validated.success) {
+ return Unexpected football data shape
;
+ }
+
+ const { matchesList, editionId, guardianBaseURL } = validated.output;
+
+ const parsedMatches = parseFootballMatches(matchesList);
+ if (!parsedMatches.ok) {
+ return Could not parse football matches
;
+ }
+
+ // Derive the competition tag (e.g. "world-cup-2026") from the first
+ // competition's URL (e.g. "/football/world-cup-2026").
+ const competitionTag =
+ matchesList[0]?.competitionMatches[0]?.competitionSummary.url.replace(
+ '/football/',
+ '',
+ ) ?? '';
+
+ return (
+
+ );
+};
diff --git a/dotcom-rendering/src/frontend/feFront.ts b/dotcom-rendering/src/frontend/feFront.ts
index edcf604da20..40448084160 100644
--- a/dotcom-rendering/src/frontend/feFront.ts
+++ b/dotcom-rendering/src/frontend/feFront.ts
@@ -3,10 +3,17 @@ import type { EditionId } from '../lib/edition';
import type { EditionBranding } from '../types/branding';
import type { StageType, Switches } from '../types/config';
import type {
+ AudioAtomBlockElement,
BoostLevel,
+ CallToActionAtomBlockElement,
+ ExplainerAtomBlockElement,
+ GuideAtomBlockElement,
Image,
Newsletter,
+ ProfileAtomBlockElement,
+ QABlockElement,
StarRating,
+ TimelineAtomBlockElement,
} from '../types/content';
import type { FooterType } from '../types/footer';
import type { FENavType } from '../types/frontend';
@@ -275,6 +282,22 @@ export type FESnap = {
embedHtml?: string;
embedCss?: string;
embedJs?: string;
+ /**
+ * Atom block elements pre-fetched by facia-press so that atoms placed on
+ * fronts (as snaps) can be rendered server-side. Each field mirrors the
+ * equivalent DCR block element for that atom type.
+ */
+ GuideAtom?: GuideAtomBlockElement;
+ QandaAtom?: QABlockElement;
+ ProfileAtom?: ProfileAtomBlockElement;
+ TimelineAtom?: TimelineAtomBlockElement;
+ AudioAtom?: AudioAtomBlockElement;
+ ExplainerAtom?: ExplainerAtomBlockElement;
+ CtaAtom?: CallToActionAtomBlockElement;
+ TempFootballCompetitionAtom?: {
+ competitionId: string;
+ componentType: string;
+ };
};
export type FEAspectRatio = '5:3' | '5:4' | '4:5' | '1:1';
diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json
index 5bebd7acf39..71081f51e2a 100644
--- a/dotcom-rendering/src/frontend/schemas/feArticle.json
+++ b/dotcom-rendering/src/frontend/schemas/feArticle.json
@@ -5854,6 +5854,48 @@
},
"embedJs": {
"type": "string"
+ },
+ "atoms": {
+ "description": "Atom block elements that have been placed on a front as a snap and\npre-fetched by facia-press so they can be rendered server-side. Each field\nmirrors the equivalent DCR block element for that atom type.",
+ "type": "object",
+ "properties": {
+ "guide": {
+ "$ref": "#/definitions/GuideAtomBlockElement"
+ },
+ "qanda": {
+ "$ref": "#/definitions/QABlockElement"
+ },
+ "profile": {
+ "$ref": "#/definitions/ProfileAtomBlockElement"
+ },
+ "timeline": {
+ "$ref": "#/definitions/TimelineAtomBlockElement"
+ },
+ "audio": {
+ "$ref": "#/definitions/AudioAtomBlockElement"
+ },
+ "explainer": {
+ "$ref": "#/definitions/ExplainerAtomBlockElement"
+ },
+ "cta": {
+ "$ref": "#/definitions/CallToActionAtomBlockElement"
+ },
+ "tempfootballcompetition": {
+ "type": "object",
+ "properties": {
+ "competitionId": {
+ "type": "string"
+ },
+ "componentType": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "competitionId",
+ "componentType"
+ ]
+ }
+ }
}
}
},
diff --git a/dotcom-rendering/src/frontend/schemas/feFront.json b/dotcom-rendering/src/frontend/schemas/feFront.json
index 08703e95381..2d58b68cea4 100644
--- a/dotcom-rendering/src/frontend/schemas/feFront.json
+++ b/dotcom-rendering/src/frontend/schemas/feFront.json
@@ -880,6 +880,43 @@
},
"embedJs": {
"type": "string"
+ },
+ "GuideAtom": {
+ "$ref": "#/definitions/GuideAtomBlockElement",
+ "description": "Atom block elements pre-fetched by facia-press so that atoms placed on\nfronts (as snaps) can be rendered server-side. Each field mirrors the\nequivalent DCR block element for that atom type."
+ },
+ "QandaAtom": {
+ "$ref": "#/definitions/QABlockElement"
+ },
+ "ProfileAtom": {
+ "$ref": "#/definitions/ProfileAtomBlockElement"
+ },
+ "TimelineAtom": {
+ "$ref": "#/definitions/TimelineAtomBlockElement"
+ },
+ "AudioAtom": {
+ "$ref": "#/definitions/AudioAtomBlockElement"
+ },
+ "ExplainerAtom": {
+ "$ref": "#/definitions/ExplainerAtomBlockElement"
+ },
+ "CtaAtom": {
+ "$ref": "#/definitions/CallToActionAtomBlockElement"
+ },
+ "TempFootballCompetitionAtom": {
+ "type": "object",
+ "properties": {
+ "competitionId": {
+ "type": "string"
+ },
+ "componentType": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "competitionId",
+ "componentType"
+ ]
}
}
},
@@ -1715,6 +1752,43 @@
},
"embedJs": {
"type": "string"
+ },
+ "GuideAtom": {
+ "$ref": "#/definitions/GuideAtomBlockElement",
+ "description": "Atom block elements pre-fetched by facia-press so that atoms placed on\nfronts (as snaps) can be rendered server-side. Each field mirrors the\nequivalent DCR block element for that atom type."
+ },
+ "QandaAtom": {
+ "$ref": "#/definitions/QABlockElement"
+ },
+ "ProfileAtom": {
+ "$ref": "#/definitions/ProfileAtomBlockElement"
+ },
+ "TimelineAtom": {
+ "$ref": "#/definitions/TimelineAtomBlockElement"
+ },
+ "AudioAtom": {
+ "$ref": "#/definitions/AudioAtomBlockElement"
+ },
+ "ExplainerAtom": {
+ "$ref": "#/definitions/ExplainerAtomBlockElement"
+ },
+ "CtaAtom": {
+ "$ref": "#/definitions/CallToActionAtomBlockElement"
+ },
+ "TempFootballCompetitionAtom": {
+ "type": "object",
+ "properties": {
+ "competitionId": {
+ "type": "string"
+ },
+ "componentType": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "competitionId",
+ "componentType"
+ ]
}
}
},
@@ -2550,6 +2624,43 @@
},
"embedJs": {
"type": "string"
+ },
+ "GuideAtom": {
+ "$ref": "#/definitions/GuideAtomBlockElement",
+ "description": "Atom block elements pre-fetched by facia-press so that atoms placed on\nfronts (as snaps) can be rendered server-side. Each field mirrors the\nequivalent DCR block element for that atom type."
+ },
+ "QandaAtom": {
+ "$ref": "#/definitions/QABlockElement"
+ },
+ "ProfileAtom": {
+ "$ref": "#/definitions/ProfileAtomBlockElement"
+ },
+ "TimelineAtom": {
+ "$ref": "#/definitions/TimelineAtomBlockElement"
+ },
+ "AudioAtom": {
+ "$ref": "#/definitions/AudioAtomBlockElement"
+ },
+ "ExplainerAtom": {
+ "$ref": "#/definitions/ExplainerAtomBlockElement"
+ },
+ "CtaAtom": {
+ "$ref": "#/definitions/CallToActionAtomBlockElement"
+ },
+ "TempFootballCompetitionAtom": {
+ "type": "object",
+ "properties": {
+ "competitionId": {
+ "type": "string"
+ },
+ "componentType": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "competitionId",
+ "componentType"
+ ]
}
}
},
@@ -3239,6 +3350,318 @@
],
"type": "string"
},
+ "GuideAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.GuideAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "img": {
+ "type": "string"
+ },
+ "html": {
+ "type": "string"
+ },
+ "credit": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "credit",
+ "elementId",
+ "html",
+ "id",
+ "label",
+ "title"
+ ]
+ },
+ "RoleType": {
+ "description": "Affects how an image is placed.\n\nAlso known as “weighting” in Composer, but we respect the CAPI naming.",
+ "enum": [
+ "halfWidth",
+ "immersive",
+ "inline",
+ "showcase",
+ "supporting",
+ "thumbnail"
+ ],
+ "type": "string"
+ },
+ "QABlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.QABlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "img": {
+ "type": "string"
+ },
+ "html": {
+ "type": "string"
+ },
+ "credit": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "credit",
+ "elementId",
+ "html",
+ "id",
+ "title"
+ ]
+ },
+ "ProfileAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.ProfileAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "img": {
+ "type": "string"
+ },
+ "html": {
+ "type": "string"
+ },
+ "credit": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "credit",
+ "elementId",
+ "html",
+ "id",
+ "label",
+ "title"
+ ]
+ },
+ "TimelineAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.TimelineAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/TimelineAtomEvent"
+ }
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "elementId",
+ "events",
+ "id",
+ "title"
+ ]
+ },
+ "TimelineAtomEvent": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "unixDate": {
+ "type": "number"
+ },
+ "body": {
+ "type": "string"
+ },
+ "toDate": {
+ "type": "string"
+ },
+ "toUnixDate": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "date",
+ "title",
+ "unixDate"
+ ]
+ },
+ "AudioAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.AudioAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "kicker": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "trackUrl": {
+ "type": "string"
+ },
+ "duration": {
+ "type": "number"
+ },
+ "coverUrl": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "coverUrl",
+ "duration",
+ "elementId",
+ "id",
+ "kicker",
+ "trackUrl"
+ ]
+ },
+ "ExplainerAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.ExplainerAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "body": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "body",
+ "elementId",
+ "id",
+ "title"
+ ]
+ },
+ "CallToActionAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.CallToActionAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "image": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "trackingCode": {
+ "type": "string"
+ },
+ "btnText": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "_type",
+ "elementId",
+ "id",
+ "title",
+ "url"
+ ]
+ },
"FEContainer": {
"enum": [
"fixed/thrasher",
@@ -3966,6 +4389,48 @@
},
"embedJs": {
"type": "string"
+ },
+ "atoms": {
+ "description": "Atom block elements that have been placed on a front as a snap and\npre-fetched by facia-press so they can be rendered server-side. Each field\nmirrors the equivalent DCR block element for that atom type.",
+ "type": "object",
+ "properties": {
+ "guide": {
+ "$ref": "#/definitions/GuideAtomBlockElement"
+ },
+ "qanda": {
+ "$ref": "#/definitions/QABlockElement"
+ },
+ "profile": {
+ "$ref": "#/definitions/ProfileAtomBlockElement"
+ },
+ "timeline": {
+ "$ref": "#/definitions/TimelineAtomBlockElement"
+ },
+ "audio": {
+ "$ref": "#/definitions/AudioAtomBlockElement"
+ },
+ "explainer": {
+ "$ref": "#/definitions/ExplainerAtomBlockElement"
+ },
+ "cta": {
+ "$ref": "#/definitions/CallToActionAtomBlockElement"
+ },
+ "tempfootballcompetition": {
+ "type": "object",
+ "properties": {
+ "competitionId": {
+ "type": "string"
+ },
+ "componentType": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "competitionId",
+ "componentType"
+ ]
+ }
+ }
}
}
},
diff --git a/dotcom-rendering/src/frontend/schemas/feTagPage.json b/dotcom-rendering/src/frontend/schemas/feTagPage.json
index b1e8e5a3b35..87f9a4f6323 100644
--- a/dotcom-rendering/src/frontend/schemas/feTagPage.json
+++ b/dotcom-rendering/src/frontend/schemas/feTagPage.json
@@ -723,6 +723,43 @@
},
"embedJs": {
"type": "string"
+ },
+ "GuideAtom": {
+ "$ref": "#/definitions/GuideAtomBlockElement",
+ "description": "Atom block elements pre-fetched by facia-press so that atoms placed on\nfronts (as snaps) can be rendered server-side. Each field mirrors the\nequivalent DCR block element for that atom type."
+ },
+ "QandaAtom": {
+ "$ref": "#/definitions/QABlockElement"
+ },
+ "ProfileAtom": {
+ "$ref": "#/definitions/ProfileAtomBlockElement"
+ },
+ "TimelineAtom": {
+ "$ref": "#/definitions/TimelineAtomBlockElement"
+ },
+ "AudioAtom": {
+ "$ref": "#/definitions/AudioAtomBlockElement"
+ },
+ "ExplainerAtom": {
+ "$ref": "#/definitions/ExplainerAtomBlockElement"
+ },
+ "CtaAtom": {
+ "$ref": "#/definitions/CallToActionAtomBlockElement"
+ },
+ "TempFootballCompetitionAtom": {
+ "type": "object",
+ "properties": {
+ "competitionId": {
+ "type": "string"
+ },
+ "componentType": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "competitionId",
+ "componentType"
+ ]
}
}
},
@@ -1467,6 +1504,318 @@
],
"type": "string"
},
+ "GuideAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.GuideAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "img": {
+ "type": "string"
+ },
+ "html": {
+ "type": "string"
+ },
+ "credit": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "credit",
+ "elementId",
+ "html",
+ "id",
+ "label",
+ "title"
+ ]
+ },
+ "RoleType": {
+ "description": "Affects how an image is placed.\n\nAlso known as “weighting” in Composer, but we respect the CAPI naming.",
+ "enum": [
+ "halfWidth",
+ "immersive",
+ "inline",
+ "showcase",
+ "supporting",
+ "thumbnail"
+ ],
+ "type": "string"
+ },
+ "QABlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.QABlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "img": {
+ "type": "string"
+ },
+ "html": {
+ "type": "string"
+ },
+ "credit": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "credit",
+ "elementId",
+ "html",
+ "id",
+ "title"
+ ]
+ },
+ "ProfileAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.ProfileAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "img": {
+ "type": "string"
+ },
+ "html": {
+ "type": "string"
+ },
+ "credit": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "credit",
+ "elementId",
+ "html",
+ "id",
+ "label",
+ "title"
+ ]
+ },
+ "TimelineAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.TimelineAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/TimelineAtomEvent"
+ }
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "elementId",
+ "events",
+ "id",
+ "title"
+ ]
+ },
+ "TimelineAtomEvent": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "unixDate": {
+ "type": "number"
+ },
+ "body": {
+ "type": "string"
+ },
+ "toDate": {
+ "type": "string"
+ },
+ "toUnixDate": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "date",
+ "title",
+ "unixDate"
+ ]
+ },
+ "AudioAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.AudioAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "kicker": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "trackUrl": {
+ "type": "string"
+ },
+ "duration": {
+ "type": "number"
+ },
+ "coverUrl": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "coverUrl",
+ "duration",
+ "elementId",
+ "id",
+ "kicker",
+ "trackUrl"
+ ]
+ },
+ "ExplainerAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.ExplainerAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "body": {
+ "type": "string"
+ },
+ "role": {
+ "$ref": "#/definitions/RoleType"
+ }
+ },
+ "required": [
+ "_type",
+ "body",
+ "elementId",
+ "id",
+ "title"
+ ]
+ },
+ "CallToActionAtomBlockElement": {
+ "type": "object",
+ "properties": {
+ "_type": {
+ "type": "string",
+ "const": "model.dotcomrendering.pageElements.CallToActionAtomBlockElement"
+ },
+ "elementId": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "image": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "trackingCode": {
+ "type": "string"
+ },
+ "btnText": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "_type",
+ "elementId",
+ "id",
+ "title",
+ "url"
+ ]
+ },
"FENavType": {
"type": "object",
"properties": {
diff --git a/dotcom-rendering/src/model/enhanceSnaps.ts b/dotcom-rendering/src/model/enhanceSnaps.ts
index 6ee8e63462a..7801465d13e 100644
--- a/dotcom-rendering/src/model/enhanceSnaps.ts
+++ b/dotcom-rendering/src/model/enhanceSnaps.ts
@@ -1,5 +1,26 @@
import type { FESnap } from '../frontend/feFront';
-import type { DCRSnapType } from '../types/front';
+import type { DCRSnapType, SnapAtoms } from '../types/front';
+
+/**
+ * Maps the atom block elements sent on a snap (each as its own optional field)
+ * into a single `SnapAtoms` object, if any are present.
+ */
+const enhanceSnapAtoms = (snap: FESnap): SnapAtoms | undefined => {
+ const atoms: SnapAtoms = {
+ guide: snap.GuideAtom,
+ qanda: snap.QandaAtom,
+ profile: snap.ProfileAtom,
+ timeline: snap.TimelineAtom,
+ audio: snap.AudioAtom,
+ explainer: snap.ExplainerAtom,
+ cta: snap.CtaAtom,
+ tempfootballcompetition: snap.TempFootballCompetitionAtom,
+ };
+
+ const hasAtom = Object.values(atoms).some((atom) => atom !== undefined);
+
+ return hasAtom ? atoms : undefined;
+};
/**
*
@@ -23,5 +44,12 @@ export const enhanceSnaps = (
);
}
+ if (dcrSnap) {
+ return {
+ ...dcrSnap,
+ atoms: enhanceSnapAtoms(dcrSnap),
+ };
+ }
+
return dcrSnap;
};
diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts
index 6c5f9bd858d..df6cabaf87e 100644
--- a/dotcom-rendering/src/types/content.ts
+++ b/dotcom-rendering/src/types/content.ts
@@ -251,7 +251,7 @@ export interface EmbedBlockElement extends ThirdPartyEmbeddedContent {
caption?: string;
}
-interface ExplainerAtomBlockElement {
+export interface ExplainerAtomBlockElement {
_type: 'model.dotcomrendering.pageElements.ExplainerAtomBlockElement';
elementId: string;
id: string;
@@ -271,7 +271,7 @@ interface GenericAtomBlockElement {
elementId: string;
}
-interface GuideAtomBlockElement {
+export interface GuideAtomBlockElement {
_type: 'model.dotcomrendering.pageElements.GuideAtomBlockElement';
elementId: string;
id: string;
@@ -554,7 +554,7 @@ export interface RecipeBlockElement {
featuredImage?: RecipeFeaturedImage;
}
-interface ProfileAtomBlockElement {
+export interface ProfileAtomBlockElement {
_type: 'model.dotcomrendering.pageElements.ProfileAtomBlockElement';
elementId: string;
id: string;
@@ -575,7 +575,7 @@ interface PullquoteBlockElement {
isThirdPartyTracking?: boolean;
}
-interface QABlockElement {
+export interface QABlockElement {
_type: 'model.dotcomrendering.pageElements.QABlockElement';
elementId: string;
id: string;
diff --git a/dotcom-rendering/src/types/front.ts b/dotcom-rendering/src/types/front.ts
index d1763598525..a9347c64879 100644
--- a/dotcom-rendering/src/types/front.ts
+++ b/dotcom-rendering/src/types/front.ts
@@ -13,7 +13,18 @@ import type {
} from '../lib/articleFormat';
import type { EditionId } from '../lib/edition';
import type { Branding, CollectionBranding } from './branding';
-import type { BoostLevel, Newsletter, StarRating } from './content';
+import type {
+ AudioAtomBlockElement,
+ BoostLevel,
+ CallToActionAtomBlockElement,
+ ExplainerAtomBlockElement,
+ GuideAtomBlockElement,
+ Newsletter,
+ ProfileAtomBlockElement,
+ QABlockElement,
+ StarRating,
+ TimelineAtomBlockElement,
+} from './content';
import type { FooterType } from './footer';
import type { FENavType } from './frontend';
import type { ArticleMedia, MainMedia } from './mainMedia';
@@ -113,10 +124,30 @@ export type DCRSlideshowImage = {
imageCaption?: string;
};
+/**
+ * Atom block elements that have been placed on a front as a snap and
+ * pre-fetched by facia-press so they can be rendered server-side. Each field
+ * mirrors the equivalent DCR block element for that atom type.
+ */
+export type SnapAtoms = {
+ guide?: GuideAtomBlockElement;
+ qanda?: QABlockElement;
+ profile?: ProfileAtomBlockElement;
+ timeline?: TimelineAtomBlockElement;
+ audio?: AudioAtomBlockElement;
+ explainer?: ExplainerAtomBlockElement;
+ cta?: CallToActionAtomBlockElement;
+ tempfootballcompetition?: {
+ competitionId: string;
+ componentType: string;
+ };
+};
+
export type DCRSnapType = {
embedHtml?: string;
embedCss?: string;
embedJs?: string;
+ atoms?: SnapAtoms;
};
export type AspectRatio = FEAspectRatio;