diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..de86122ce24 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.github +node_modules +dist diff --git a/.github/workflows/ab-testing-ci.yml b/.github/workflows/ab-testing-ci.yml index 7689dfb593b..39184a139fc 100644 --- a/.github/workflows/ab-testing-ci.yml +++ b/.github/workflows/ab-testing-ci.yml @@ -194,7 +194,7 @@ jobs: run: pnpm --filter @guardian/ab-testing-cdk synth - name: Riff-Raff Upload - uses: guardian/actions-riff-raff@v4.3.3 + uses: guardian/actions-riff-raff@v4.3.4 with: roleArn: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} githubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index ceb24e9681e..85eb39f5265 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -4,10 +4,21 @@ on: push: jobs: + production-container: + permissions: + contents: read + id-token: write # Required to exchange for AWS credentials using OIDC + uses: ./.github/workflows/container-production.yml + secrets: + GU_RIFF_RAFF_ROLE_ARN: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} + container: permissions: packages: write + needs: [production-container] uses: ./.github/workflows/container.yml + with: + production-image-digest: ${{ needs.production-container.outputs.imageDigest }} prettier: uses: ./.github/workflows/prettier.yml diff --git a/.github/workflows/container-production.yml b/.github/workflows/container-production.yml new file mode 100644 index 00000000000..0ecf3611a2f --- /dev/null +++ b/.github/workflows/container-production.yml @@ -0,0 +1,46 @@ +name: Production container +on: + workflow_call: + secrets: + GU_RIFF_RAFF_ROLE_ARN: + required: true + outputs: + imageDigest: + description: 'The digest of the generated container image' + value: ${{ jobs.build-production-image.outputs.imageDigest }} +jobs: + facts: + runs-on: ubuntu-slim + permissions: {} # This job doesn't need any permissions. Explicitly set it to an empty object to avoid inheriting any default permissions of the workflow. + outputs: + branchName: ${{ steps.get-build-facts.outputs.branchName }} + buildNumber: ${{ steps.get-build-facts.outputs.buildNumber }} + commitSha: ${{ steps.get-build-facts.outputs.commitSha }} + steps: + - uses: guardian/actions-build-facts@v0.0.1 + id: get-build-facts + + build-production-image: + runs-on: ubuntu-latest + needs: + - facts + permissions: + contents: read + id-token: write # Required to exchange for AWS credentials using OIDC + outputs: + imageDigest: ${{ steps.publish-image.outputs.imageDigest }} + steps: + - uses: actions/checkout@v6.0.2 + - name: Add commit hash for PRout + working-directory: dotcom-rendering + run: echo 'export const GIT_COMMIT_HASH = "${{ needs.facts.outputs.commitSha }}";' > src/server/prout.ts + - name: Build image + run: docker buildx build -f Production.dockerfile -t ${{ github.repository }}:latest . + - name: Publish Image + uses: guardian/actions-publish-image@v0.0.2 + id: publish-image + with: + roleArn: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} + branchName: ${{ needs.facts.outputs.branchName }} + buildNumber: ${{ needs.facts.outputs.buildNumber }} + commitSha: ${{ needs.facts.outputs.commitSha }} diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 6ac78859080..414ca4175bf 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -2,6 +2,11 @@ # Commercial rely on a container image built from the main branch with the tag 'main' on: workflow_call: + inputs: + production-image-digest: + description: 'Digest of image pushed to AWS ECR to run on AWS ECS. Gets used by `cdk synth`.' + required: true + type: string outputs: container-image: description: 'The generated container image path' @@ -30,6 +35,8 @@ jobs: echo 'export const GIT_COMMIT_HASH = "${{ github.sha }}";' > src/server/prout.ts - name: Generate production bundle + env: + IMAGE_DIGEST: ${{ inputs.production-image-digest }} run: make riffraff-bundle working-directory: dotcom-rendering diff --git a/.github/workflows/dcr-chromatic.yml b/.github/workflows/dcr-chromatic.yml index 03af91631b6..ea378ab324b 100644 --- a/.github/workflows/dcr-chromatic.yml +++ b/.github/workflows/dcr-chromatic.yml @@ -47,7 +47,7 @@ jobs: - name: Chromatic - DCR env: NODE_OPTIONS: '--max_old_space_size=4096' - uses: chromaui/action@v16.10.0 + uses: chromaui/action@v17.4.1 if: | github.ref == 'refs/heads/main' || (github.event_name == 'pull_request' && diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ebd53b5f4a3..c86215df233 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -55,7 +55,7 @@ jobs: # All rendering apps deployment - name: Upload rendering apps to RiffRaff - uses: guardian/actions-riff-raff@v4.3.3 + uses: guardian/actions-riff-raff@v4.3.4 with: githubToken: ${{ secrets.GITHUB_TOKEN }} roleArn: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} diff --git a/Production.dockerfile b/Production.dockerfile new file mode 100644 index 00000000000..405bd27dc9b --- /dev/null +++ b/Production.dockerfile @@ -0,0 +1,31 @@ +FROM dhi.io/node:24-alpine3.23-dev AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME/bin:$PATH" +RUN corepack enable +COPY . /app +WORKDIR /app + +# Install dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /pnpm/store to speed up subsequent builds. +FROM base AS dependencies +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +WORKDIR /app/dotcom-rendering +ENV PATH="node_modules/.bin:$PATH" +ENV NODE_ENV=production + +# Build the application +FROM dependencies AS builder +RUN webpack --config webpack/webpack.config.js --progress +RUN node scripts/islands/island-descriptions.mjs + +# Finally, create the production image with only the necessary files +FROM dhi.io/node:24-alpine3.23 AS application +WORKDIR /app +COPY --from=builder --chown=node:node /app/dotcom-rendering/dist /app + +ENV NODE_ENV=production + +# Expose the port that the application listens on +EXPOSE 9000 + +CMD ["node", "/app/server.js"] diff --git a/ab-testing/cdk/lib/__snapshots__/abTestingConfig.test.ts.snap b/ab-testing/cdk/lib/__snapshots__/abTestingConfig.test.ts.snap index 872631bab4b..49cd9230f12 100644 --- a/ab-testing/cdk/lib/__snapshots__/abTestingConfig.test.ts.snap +++ b/ab-testing/cdk/lib/__snapshots__/abTestingConfig.test.ts.snap @@ -2,7 +2,7 @@ exports[`The ID5 Baton Lambda stack > matches the CODE snapshot 1`] = ` { "Metadata": { "gu:cdk:constructs": [], - "gu:cdk:version": "63.3.1" + "gu:cdk:version": "64.0.0" }, "Parameters": { "BuildId": { @@ -46,7 +46,7 @@ exports[`The ID5 Baton Lambda stack > matches the PROD snapshot 1`] = ` { "Metadata": { "gu:cdk:constructs": [], - "gu:cdk:version": "63.3.1" + "gu:cdk:version": "64.0.0" }, "Parameters": { "BuildId": { diff --git a/ab-testing/cdk/lib/__snapshots__/deploymentLambda.test.ts.snap b/ab-testing/cdk/lib/__snapshots__/deploymentLambda.test.ts.snap index 4e5a873bb48..965c6888273 100644 --- a/ab-testing/cdk/lib/__snapshots__/deploymentLambda.test.ts.snap +++ b/ab-testing/cdk/lib/__snapshots__/deploymentLambda.test.ts.snap @@ -5,7 +5,7 @@ exports[`The AB testing deployment lambda stack > matches the CODE snapshot 1`] "GuDistributionBucketParameter", "GuLambdaFunction" ], - "gu:cdk:version": "63.3.1" + "gu:cdk:version": "64.0.0" }, "Parameters": { "SsmParameterValueaccountservicesdotcomstorebucketC96584B6F00A464EAD1953AFF4B05118Parameter": { @@ -55,7 +55,7 @@ exports[`The AB testing deployment lambda stack > matches the CODE snapshot 1`] }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -294,7 +294,7 @@ exports[`The AB testing deployment lambda stack > matches the CODE snapshot 1`] }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -327,7 +327,7 @@ exports[`The AB testing deployment lambda stack > matches the PROD snapshot 1`] "GuDistributionBucketParameter", "GuLambdaFunction" ], - "gu:cdk:version": "63.3.1" + "gu:cdk:version": "64.0.0" }, "Parameters": { "SsmParameterValueaccountservicesdotcomstorebucketC96584B6F00A464EAD1953AFF4B05118Parameter": { @@ -377,7 +377,7 @@ exports[`The AB testing deployment lambda stack > matches the PROD snapshot 1`] }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -616,7 +616,7 @@ exports[`The AB testing deployment lambda stack > matches the PROD snapshot 1`] }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", diff --git a/ab-testing/cdk/lib/__snapshots__/notificationLambda.test.ts.snap b/ab-testing/cdk/lib/__snapshots__/notificationLambda.test.ts.snap index c4a6f4d5c3b..d5b734cb545 100644 --- a/ab-testing/cdk/lib/__snapshots__/notificationLambda.test.ts.snap +++ b/ab-testing/cdk/lib/__snapshots__/notificationLambda.test.ts.snap @@ -10,7 +10,7 @@ exports[`The AB testing notification lambda stack > matches the CODE snapshot 1` "GuScheduledLambda", "GuLambdaErrorPercentageAlarm" ], - "gu:cdk:version": "63.3.1" + "gu:cdk:version": "64.0.0" }, "Resources": { "EmailIdentityAbtestingnotificationlambdaE053C648": { @@ -24,7 +24,7 @@ exports[`The AB testing notification lambda stack > matches the CODE snapshot 1` }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -113,7 +113,7 @@ exports[`The AB testing notification lambda stack > matches the CODE snapshot 1` "Tags": [ { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -166,7 +166,7 @@ exports[`The AB testing notification lambda stack > matches the CODE snapshot 1` }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -340,7 +340,7 @@ exports[`The AB testing notification lambda stack > matches the CODE snapshot 1` }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -452,7 +452,7 @@ exports[`The AB testing notification lambda stack > matches the CODE snapshot 1` "Tags": [ { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -494,7 +494,7 @@ exports[`The AB testing notification lambda stack > matches the PROD snapshot 1` "GuScheduledLambda", "GuLambdaErrorPercentageAlarm" ], - "gu:cdk:version": "63.3.1" + "gu:cdk:version": "64.0.0" }, "Resources": { "EmailIdentityAbtestingnotificationlambdaE053C648": { @@ -508,7 +508,7 @@ exports[`The AB testing notification lambda stack > matches the PROD snapshot 1` }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -597,7 +597,7 @@ exports[`The AB testing notification lambda stack > matches the PROD snapshot 1` "Tags": [ { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -660,7 +660,7 @@ exports[`The AB testing notification lambda stack > matches the PROD snapshot 1` }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -834,7 +834,7 @@ exports[`The AB testing notification lambda stack > matches the PROD snapshot 1` }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -869,7 +869,7 @@ exports[`The AB testing notification lambda stack > matches the PROD snapshot 1` }, { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", @@ -1006,7 +1006,7 @@ exports[`The AB testing notification lambda stack > matches the PROD snapshot 1` "Tags": [ { "Key": "gu:cdk:version", - "Value": "63.3.1" + "Value": "64.0.0" }, { "Key": "gu:repo", diff --git a/ab-testing/cdk/tsconfig.json b/ab-testing/cdk/tsconfig.json index 9f754911c89..be4e2f34ece 100644 --- a/ab-testing/cdk/tsconfig.json +++ b/ab-testing/cdk/tsconfig.json @@ -6,7 +6,8 @@ "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "moduleResolution": "nodenext", - "module": "nodenext" + "module": "nodenext", + "types": ["node"] }, "exclude": ["node_modules"] } diff --git a/ab-testing/config/abTests.ts b/ab-testing/config/abTests.ts index 4178fe6fb3f..d3414585345 100644 --- a/ab-testing/config/abTests.ts +++ b/ab-testing/config/abTests.ts @@ -57,18 +57,6 @@ const ABTests: ABTest[] = [ groups: ["control"], shouldForceMetricsCollection: false, }, - { - name: "commercial-hosted-gallery", - description: "Preview the Hosted Gallery pages using dotcom-rendering", - owners: ["commercial.dev@guardian.co.uk"], - expirationDate: "2026-07-01", - type: "server", - status: "ON", - audienceSize: 0 / 100, - audienceSpace: "A", - groups: ["preview"], - shouldForceMetricsCollection: false, - }, { name: "growth-auxia-banner", description: "Use Auxia API for deciding when to show a RR banner", @@ -81,83 +69,69 @@ const ABTests: ABTest[] = [ groups: ["control", "variant"], shouldForceMetricsCollection: true, }, - { - name: "newsletters-signup-card-country-illustration", - description: - "Compare the existing SecureSignup (control) against a slightly modified version of the control (variantNewField) and the new NewsletterSignupCard design (variantIllustratedCard)", - owners: ["newsletters.dev@guardian.co.uk"], - expirationDate: "2026-07-01", - type: "client", - status: "ON", - audienceSize: 1, - audienceSpace: "B", - groups: ["control", "variantNewField", "variantIllustratedCard"], - shouldForceMetricsCollection: false, - }, { name: "commercial-prebid-price-floor-holdback", description: "This test will be the 5% holdback group for the prebid price floor", owners: ["commercial.dev@guardian.co.uk"], - expirationDate: "2026-06-17", + expirationDate: "2026-07-16", type: "client", - status: "ON", + status: "OFF", audienceSize: 5 / 100, audienceSpace: "A", groups: ["holdback"], shouldForceMetricsCollection: true, }, { - name: "commercial-user-module-intentIq", + name: "commercial-mobile-sticky-liveblog-us", description: - "Holdback test to measure the impact of adding intentIq as an ID partner in the user module.", + "Holdback test, where variant is the 'holdback' group, to measure uplift in adding the mobile-sticky slot for Liveblogs articles in the US.", owners: ["commercial.dev@guardian.co.uk"], - expirationDate: "2026-06-18", + expirationDate: "2026-07-22", type: "client", status: "ON", - audienceSize: 10 / 100, + audienceSize: 5 / 100, audienceSpace: "A", groups: ["control", "variant"], shouldForceMetricsCollection: true, }, { - name: "commercial-user-module-intentIq-us", + name: "commercial-teads-prebid", description: - "Holdback test to measure the impact of adding intentIq as an ID partner in the user module for users in the US", + "Test to measure the impact of adding Teads as a bidder in prebid .", owners: ["commercial.dev@guardian.co.uk"], - expirationDate: "2026-06-18", + expirationDate: "2026-06-11", type: "client", - status: "ON", - audienceSize: 10 / 100, + status: "OFF", + audienceSize: 0 / 100, audienceSpace: "A", - groups: ["control", "holdback"], + groups: ["control", "variant"], shouldForceMetricsCollection: true, }, { - name: "commercial-teads-prebid", + name: "commercial-full-width-hold-back", description: - "Test to measure the impact of adding Teads as a bidder in prebid .", + "Test to measure impact of adding full width to spacefinder", owners: ["commercial.dev@guardian.co.uk"], - expirationDate: "2026-06-11", + expirationDate: "2026-07-14", type: "client", status: "ON", - audienceSize: 0 / 100, + audienceSize: 5 / 100, audienceSpace: "A", - groups: ["control", "variant"], + groups: ["holdback", "control"], shouldForceMetricsCollection: true, }, { - name: "newsletters-highlights-signup-card", - description: - "Test a new newsletter signup card design in the scrollable/highlights container. Only users in the enabled group see the newsletter card; for all other users, newsletter trails are hidden during this rollout phase.", - owners: ["newsletters.dev@guardian.co.uk"], + name: "commercial-ozone-outstream", + description: "A test to ensure correct integration of Ozone outstream.", + owners: ["commercial.dev@guardian.co.uk"], + expirationDate: "2026-09-23", + type: "client", status: "ON", - expirationDate: "2026-12-31", - type: "server", - audienceSize: 0, - audienceSpace: "C", - groups: ["enable"], - shouldForceMetricsCollection: true, + audienceSize: 0 / 100, + audienceSpace: "A", + groups: ["control", "variant"], + shouldForceMetricsCollection: false, }, { name: "fronts-and-curation-loop-click-through", @@ -165,7 +139,19 @@ const ABTests: ABTest[] = [ "Test impact of click to article via loop videos on fronts", owners: ["fronts.and.curation@guardian.co.uk"], status: "ON", - expirationDate: "2026-06-19", + expirationDate: "2026-07-19", + type: "server", + audienceSize: 5 / 100, + audienceSpace: "A", + groups: ["control", "variant"], + shouldForceMetricsCollection: false, + }, + { + name: "fronts-and-curation-click-to-play", + description: "Test click to play longform videos vs autoplay", + owners: ["fronts.and.curation@guardian.co.uk"], + status: "OFF", + expirationDate: "2026-07-28", type: "server", audienceSize: 0 / 100, groups: ["control", "variant"], @@ -184,6 +170,30 @@ const ABTests: ABTest[] = [ groups: ["control", "variant"], shouldForceMetricsCollection: false, }, + { + name: "feast-recipe-nudge-v2", + description: + "Measures the impact of showing the Feast contextual nudge on recipe article pages", + owners: ["feast@theguardian.com"], + status: "ON", + expirationDate: "2027-01-01", + type: "client", + audienceSize: 1, + audienceSpace: "B", + groups: ["control", "variant-1"], + shouldForceMetricsCollection: false, + }, + { + name: "webx-cricket-redesign", + description: "Redesign of the cricket header and scorecard on web", + owners: ["dotcom.platform@theguardian.com"], + status: "ON", + expirationDate: "2026-08-01", + type: "server", + audienceSize: 0 / 100, + groups: ["enable"], + shouldForceMetricsCollection: false, + }, ]; const activeABtests = ABTests.filter((test) => test.status === "ON"); diff --git a/ab-testing/config/tsconfig.json b/ab-testing/config/tsconfig.json index 925fbd5ee96..52e5f7d21fd 100644 --- a/ab-testing/config/tsconfig.json +++ b/ab-testing/config/tsconfig.json @@ -6,7 +6,8 @@ "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "moduleResolution": "nodenext", - "module": "nodenext" + "module": "nodenext", + "types": ["node"] }, "exclude": ["node_modules", "index.ts"] } diff --git a/ab-testing/config/types.ts b/ab-testing/config/types.ts index c705429674f..bfbd0fcbd78 100644 --- a/ab-testing/config/types.ts +++ b/ab-testing/config/types.ts @@ -13,6 +13,7 @@ type Team = | "newsletters" | "fronts-and-curation" | "growth" + | "feast" | "martech"; type TestName = `${Team}-${string}`; diff --git a/ab-testing/deploy-lambda/tsconfig.json b/ab-testing/deploy-lambda/tsconfig.json index bf4067ab370..a5c7526c057 100644 --- a/ab-testing/deploy-lambda/tsconfig.json +++ b/ab-testing/deploy-lambda/tsconfig.json @@ -4,7 +4,8 @@ "allowImportingTsExtensions": true, "allowArbitraryExtensions": false, "erasableSyntaxOnly": true, - "noEmit": true + "noEmit": true, + "types": ["node"] }, "exclude": ["node_modules"] } diff --git a/ab-testing/frontend/package.json b/ab-testing/frontend/package.json index 430f1f3b23a..9e971c218a1 100644 --- a/ab-testing/frontend/package.json +++ b/ab-testing/frontend/package.json @@ -13,13 +13,13 @@ }, "devDependencies": { "@guardian/ab-testing-config": "workspace:ab-testing-config", - "@sveltejs/adapter-auto": "6.1.1", + "@sveltejs/adapter-auto": "7.0.1", "@sveltejs/adapter-static": "3.0.10", "@sveltejs/kit": "2.60.1", - "@sveltejs/vite-plugin-svelte": "5.1.1", - "svelte": "5.55.7", - "svelte-check": "4.3.3", + "@sveltejs/vite-plugin-svelte": "7.1.2", + "svelte": "5.56.3", + "svelte-check": "4.6.0", "typescript": "catalog:", - "vite": "6.4.2" + "vite": "6.4.3" } } diff --git a/ab-testing/notification-lambda/tsconfig.json b/ab-testing/notification-lambda/tsconfig.json index bf4067ab370..a5c7526c057 100644 --- a/ab-testing/notification-lambda/tsconfig.json +++ b/ab-testing/notification-lambda/tsconfig.json @@ -4,7 +4,8 @@ "allowImportingTsExtensions": true, "allowArbitraryExtensions": false, "erasableSyntaxOnly": true, - "noEmit": true + "noEmit": true, + "types": ["node"] }, "exclude": ["node_modules"] } diff --git a/ab-testing/tsconfig.json b/ab-testing/tsconfig.json index 9f754911c89..be4e2f34ece 100644 --- a/ab-testing/tsconfig.json +++ b/ab-testing/tsconfig.json @@ -6,7 +6,8 @@ "allowImportingTsExtensions": true, "erasableSyntaxOnly": true, "moduleResolution": "nodenext", - "module": "nodenext" + "module": "nodenext", + "types": ["node"] }, "exclude": ["node_modules"] } diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000000..3fd54642df3 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,53 @@ +# A Docker Compose file for running the production build locally. +# Usage: +# docker compose up -d --build --force-recreate +services: + tag-page-rendering: + build: + dockerfile: ./Production.dockerfile + environment: + NODE_ENV: production + GU_STAGE: PROD + GU_APP: tag-page-rendering + GU_STACK: frontend + + # We configure Log4JS based on this environment variable. See `server/lib/logging.ts`. + AWS_EXECUTION_ENV: AWS_ECS_LOCAL + + # Explicitly tell AWS SDK where to find credentials + AWS_SHARED_CREDENTIALS_FILE: /.aws/credentials + ports: + - '9000:9000' + + # Share the host's AWS credentials with the container + volumes: + - ${HOME}/.aws/credentials:/.aws/credentials:ro + + # In Production.dockerfile, we're deliberately using a minimal image that does not have `curl` installed. + # Therefore, we're using Node to make a request to the healthcheck endpoint instead of using `curl`. + healthcheck: + test: + [ + 'CMD', + 'node', + '-e', + "fetch('http://localhost:9000/_healthcheck').then(_ => process.exit(0)).catch(_ => process.exit(1))", + ] + interval: 10s + timeout: 5s + retries: 5 + + # This service is used to make a sample request to tag-page-rendering only after it has started and is healthy. + # It exits immediately after making the request, so it is not a long-running service. + # View the logs via: + # docker logs "$(docker ps -aq --filter "name=sample-request" --latest)" + # The log output is the DCR response, so we can also pipe it through `jq` to pretty-print it, e.g.: + # docker logs "$(docker ps -aq --filter "name=sample-request" --latest)" | jq . + sample-request: + image: curlimages/curl:8.21.0 + depends_on: + tag-page-rendering: + condition: service_healthy + command: | + curl "https://www.theguardian.com/tone/minutebyminute.json?dcr=true" --silent > data.json && \ + curl -X POST http://localhost:9000/TagPage -d @data.json -H "Content-Type: application/json" diff --git a/dotcom-rendering/.gitignore b/dotcom-rendering/.gitignore index 0975586c5f1..8211ed94307 100644 --- a/dotcom-rendering/.gitignore +++ b/dotcom-rendering/.gitignore @@ -4,3 +4,6 @@ tsconfig.tsbuildinfo # CDK asset staging directory .cdk.staging cdk.out + +# SWC cache +.swc diff --git a/dotcom-rendering/.storybook/decorators/themeDecorator.tsx b/dotcom-rendering/.storybook/decorators/themeDecorator.tsx index f38de153fb9..ed68aedbf86 100644 --- a/dotcom-rendering/.storybook/decorators/themeDecorator.tsx +++ b/dotcom-rendering/.storybook/decorators/themeDecorator.tsx @@ -14,6 +14,7 @@ import { import type { CSSProperties } from 'react'; import type { ArticleFormat } from '../../src/lib/articleFormat'; import { storybookPaletteDeclarations as paletteDeclarations } from '../mocks/paletteDeclarations'; +import { hostedPaletteOverrides } from '../../src/lib/hostedContentStyles'; const darkStoryCss = css` background-color: ${sourcePalette.neutral[0]}; @@ -113,3 +114,33 @@ export const browserThemeDecorator = ); + +/** + * Colour scheme decorator specifically for hosted content pages, + * where the accent colour from the branding overrides some palette colours + */ +export const hostedPaletteDecorator = + (accentColour: string): Decorator => + (Story, context) => ( +
+ +
+ ); diff --git a/dotcom-rendering/cdk/bin/cdk.ts b/dotcom-rendering/cdk/bin/cdk.ts index 35cda5b0d77..ed16e84ea4e 100644 --- a/dotcom-rendering/cdk/bin/cdk.ts +++ b/dotcom-rendering/cdk/bin/cdk.ts @@ -1,5 +1,6 @@ import { App } from 'aws-cdk-lib'; import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; +import type { RenderingCDKStackProps } from '../lib/renderingStack'; import { RenderingCDKStack } from '../lib/renderingStack'; const cdkApp = new App(); @@ -96,13 +97,21 @@ new RenderingCDKStack(cdkApp, 'FaciaRendering-PROD', { }); /** Tag pages */ -new RenderingCDKStack(cdkApp, 'TagPageRendering-CODE', { +export const TagPageRenderingPropsCODE: RenderingCDKStackProps = { guApp: 'tag-page-rendering', stage: 'CODE', domainName: 'tag-page-rendering.code.dev-guardianapis.com', scaling: { minimumInstances: 1, maximumInstances: 3 }, instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.SMALL), -}); + imageIdentifier: process.env.IMAGE_DIGEST ?? 'DEV', +}; + +new RenderingCDKStack( + cdkApp, + 'TagPageRendering-CODE', + TagPageRenderingPropsCODE, +); + new RenderingCDKStack(cdkApp, 'TagPageRendering-PROD', { guApp: 'tag-page-rendering', stage: 'PROD', diff --git a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap index 02c706ff07f..6bcde3cadfb 100644 --- a/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap +++ b/dotcom-rendering/cdk/lib/__snapshots__/renderingStack.test.ts.snap @@ -11,8 +11,7 @@ exports[`The RenderingCDKStack matches the snapshot 1`] = ` "GuVpcParameter", "GuSubnetListParameter", "GuSubnetListParameter", - "GuEc2App", - "GuCertificate", + "GuLoadBalancedAppExperimental", "GuInstanceRole", "GuSsmSshPolicy", "GuDescribeEC2Policy", @@ -23,9 +22,10 @@ exports[`The RenderingCDKStack matches the snapshot 1`] = ` "GuAmiParameter", "GuHttpsEgressSecurityGroup", "GuAutoScalingGroup", + "GuApplicationTargetGroup", + "GuCertificate", "GuApplicationLoadBalancer", "GuAccessLoggingBucketParameter", - "GuApplicationTargetGroup", "GuHttpsApplicationListener", "GuSecurityGroup", "GuAlb5xxPercentageAlarm", @@ -1614,3 +1614,1771 @@ systemctl start article-rendering", }, } `; + +exports[`The RenderingCDKStack matches the snapshot for Tag Page Rendering CODE (uses ECS) 1`] = ` +{ + "Metadata": { + "gu:cdk:constructs": [ + "GuDistributionBucketParameter", + "GuAllowPolicy", + "GuAllowPolicy", + "GuAllowPolicy", + "GuVpcParameter", + "GuSubnetListParameter", + "GuSubnetListParameter", + "GuLoadBalancedAppExperimental", + "GuInstanceRole", + "GuSsmSshPolicy", + "GuDescribeEC2Policy", + "GuLoggingStreamNameParameter", + "GuLogShippingPolicy", + "GuGetDistributablePolicy", + "GuParameterStoreReadPolicy", + "GuAmiParameter", + "GuHttpsEgressSecurityGroup", + "GuAutoScalingGroup", + "GuApplicationTargetGroup", + "GuRiffRaffDeploymentIdParameterExperimental", + "GuHttpsEgressSecurityGroup", + "GuApplicationTargetGroup", + "GuCertificate", + "GuApplicationLoadBalancer", + "GuAccessLoggingBucketParameter", + "GuHttpsApplicationListener", + "GuSecurityGroup", + "GuCname", + ], + "gu:cdk:version": "TEST", + }, + "Outputs": { + "LoadBalancerTagpagerenderingDnsName": { + "Description": "DNS entry for LoadBalancerTagpagerendering", + "Value": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "DNSName", + ], + }, + }, + }, + "Parameters": { + "AMITagpagerendering": { + "Description": "Amazon Machine Image ID for the app tag-page-rendering. Use this in conjunction with AMIgo to keep AMIs up to date.", + "Type": "AWS::EC2::Image::Id", + }, + "AccessLoggingBucket": { + "Default": "/account/services/access-logging/bucket", + "Description": "S3 bucket to store your access logs", + "Type": "AWS::SSM::Parameter::Value", + }, + "DeployToolsAccountIdParameter": { + "Default": "/organisation/accounts/deployTools", + "Type": "AWS::SSM::Parameter::Value", + }, + "DistributionBucketName": { + "Default": "/account/services/artifact.bucket", + "Description": "SSM parameter containing the S3 bucket name holding distribution artifacts", + "Type": "AWS::SSM::Parameter::Value", + }, + "LoggingStreamName": { + "Default": "/account/services/logging.stream.name", + "Description": "SSM parameter containing the Name (not ARN) on the kinesis stream", + "Type": "AWS::SSM::Parameter::Value", + }, + "RiffRaffDeploymentId": { + "Description": "Used by Riff-Raff to inject the deployment ID.", + "Type": "String", + }, + "VpcId": { + "Default": "/account/vpc/primary/id", + "Description": "Virtual Private Cloud to run EC2 instances within. Should NOT be the account default VPC.", + "Type": "AWS::SSM::Parameter::Value", + }, + "tagpagerenderingPrivateSubnets": { + "Default": "/account/vpc/primary/subnets/private", + "Description": "A list of private subnets", + "Type": "AWS::SSM::Parameter::Value>", + }, + "tagpagerenderingPublicSubnets": { + "Default": "/account/vpc/primary/subnets/public", + "Description": "A list of public subnets", + "Type": "AWS::SSM::Parameter::Value>", + }, + }, + "Resources": { + "AllowPolicyCloudwatchLogsA783E5B4": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "cloudwatch:*", + "logs:*", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyCloudwatchLogsA783E5B4", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "AllowPolicyDescribeDecryptKmsE91286F3": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:kms:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":FrontendConfigKey", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyDescribeDecryptKmsE91286F3", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "AllowPolicyGetSsmParamsByPathB54B2DE8": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ssm:GetParametersByPath", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/frontend/*", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/dotcom/*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "AllowPolicyGetSsmParamsByPathB54B2DE8", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "AutoScalingGroupTagpagerenderingASG7F7E0748": { + "Properties": { + "HealthCheckGracePeriod": 120, + "HealthCheckType": "ELB", + "LaunchTemplate": { + "LaunchTemplateId": { + "Ref": "frontendCODEtagpagerendering3C218821", + }, + "Version": { + "Fn::GetAtt": [ + "frontendCODEtagpagerendering3C218821", + "LatestVersionNumber", + ], + }, + }, + "MaxSize": "3", + "MetricsCollection": [ + { + "Granularity": "1Minute", + }, + ], + "MinSize": "1", + "Tags": [ + { + "Key": "App", + "PropagateAtLaunch": true, + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "PropagateAtLaunch": true, + "Value": "TEST", + }, + { + "Key": "gu:repo", + "PropagateAtLaunch": true, + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "LogKinesisStreamName", + "PropagateAtLaunch": true, + "Value": { + "Ref": "LoggingStreamName", + }, + }, + { + "Key": "Stack", + "PropagateAtLaunch": true, + "Value": "frontend", + }, + { + "Key": "Stage", + "PropagateAtLaunch": true, + "Value": "CODE", + }, + { + "Key": "SystemdUnit", + "PropagateAtLaunch": true, + "Value": "tag-page-rendering.service", + }, + ], + "TargetGroupARNs": [ + { + "Ref": "TargetGroupTagpagerendering42E428EE", + }, + ], + "VPCZoneIdentifier": { + "Ref": "tagpagerenderingPrivateSubnets", + }, + }, + "Type": "AWS::AutoScaling::AutoScalingGroup", + }, + "CertificateTagpagerendering5182FE63": { + "DeletionPolicy": "Retain", + "Properties": { + "DomainName": "tag-page-rendering.code.dev-guardianapis.com", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-CODE/CertificateTagpagerendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "ValidationMethod": "DNS", + }, + "Type": "AWS::CertificateManager::Certificate", + "UpdateReplacePolicy": "Retain", + }, + "DescribeEC2PolicyFF5F9295": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "autoscaling:DescribeAutoScalingInstances", + "autoscaling:DescribeAutoScalingGroups", + "ec2:DescribeTags", + "ec2:DescribeInstances", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "describe-ec2-policy", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "EcsService81FC6EF6": { + "DependsOn": [ + "EcsTaskDefinitionTaskRoleB7B6D8DD", + "ListenerTagpagerendering92E57078", + ], + "Properties": { + "Cluster": { + "Ref": "tagpagerenderingEcsClusterE7696595", + }, + "DeploymentConfiguration": { + "Alarms": { + "AlarmNames": [], + "Enable": false, + "Rollback": false, + }, + "DeploymentCircuitBreaker": { + "Enable": true, + "Rollback": true, + }, + "MaximumPercent": 200, + "MinimumHealthyPercent": 100, + }, + "DeploymentController": { + "Type": "ECS", + }, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "tag-page-rendering", + "ContainerPort": 9000, + "TargetGroupArn": { + "Ref": "EcsTargetGroupTagpagerendering06213654", + }, + }, + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "tagpagerenderingPrivateSubnets", + }, + }, + }, + "PropagateTags": "SERVICE", + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TaskDefinition": { + "Ref": "EcsTaskDefinition63157ED3", + }, + }, + "Type": "AWS::ECS::Service", + }, + "EcsServiceTaskCountTarget02FCCE22": { + "DependsOn": [ + "EcsTaskDefinitionTaskRoleB7B6D8DD", + ], + "Properties": { + "MaxCapacity": 2, + "MinCapacity": 1, + "ResourceId": { + "Fn::Join": [ + "", + [ + "service/", + { + "Ref": "tagpagerenderingEcsClusterE7696595", + }, + "/", + { + "Fn::GetAtt": [ + "EcsService81FC6EF6", + "Name", + ], + }, + ], + ], + }, + "RoleARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::", + { + "Ref": "AWS::AccountId", + }, + ":role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService", + ], + ], + }, + "ScalableDimension": "ecs:service:DesiredCount", + "ServiceNamespace": "ecs", + }, + "Type": "AWS::ApplicationAutoScaling::ScalableTarget", + }, + "EcsTargetGroupTagpagerendering06213654": { + "Properties": { + "HealthCheckIntervalSeconds": 10, + "HealthCheckPath": "/_healthcheck", + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": 5, + "HealthyThresholdCount": 5, + "Port": 9000, + "Protocol": "HTTP", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TargetGroupAttributes": [ + { + "Key": "deregistration_delay.timeout_seconds", + "Value": "30", + }, + { + "Key": "stickiness.enabled", + "Value": "false", + }, + ], + "TargetType": "ip", + "UnhealthyThresholdCount": 2, + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + "EcsTaskDefinition63157ED3": { + "Properties": { + "ContainerDefinitions": [ + { + "DockerLabels": { + "RiffRaffDeploymentId": { + "Ref": "RiffRaffDeploymentId", + }, + }, + "Environment": [ + { + "Name": "NODE_ENV", + "Value": "production", + }, + { + "Name": "GU_STAGE", + "Value": "CODE", + }, + { + "Name": "GU_APP", + "Value": "tag-page-rendering", + }, + { + "Name": "GU_STACK", + "Value": "frontend", + }, + ], + "Essential": true, + "Image": { + "Fn::Join": [ + "", + [ + { + "Ref": "DeployToolsAccountIdParameter", + }, + ".dkr.ecr.eu-west-1.", + { + "Ref": "AWS::URLSuffix", + }, + "/guardian/dotcom-rendering@sha256:12345", + ], + ], + }, + "LogConfiguration": { + "LogDriver": "awsfirelens", + "Options": { + "Name": "kinesis_streams", + "region": "eu-west-1", + "retry_limit": "2", + "stream": { + "Ref": "LoggingStreamName", + }, + }, + }, + "Name": "tag-page-rendering", + "PortMappings": [ + { + "ContainerPort": 9000, + "Protocol": "tcp", + }, + ], + "ReadonlyRootFilesystem": true, + "VersionConsistency": "disabled", + }, + { + "Environment": [ + { + "Name": "STACK", + "Value": "frontend", + }, + { + "Name": "STAGE", + "Value": "CODE", + }, + { + "Name": "APP", + "Value": "tag-page-rendering", + }, + { + "Name": "TASK_NAME", + "Value": "tag-page-rendering", + }, + { + "Name": "GU_REPO", + "Value": "guardian/dotcom-rendering", + }, + ], + "Essential": true, + "FirelensConfiguration": { + "Type": "fluentbit", + }, + "Image": "ghcr.io/guardian/devx-logs@sha256:cf91724a5166f1c143e07958820aa2122afb61c164b68555d15cb92abb5acda0", + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "EcsTaskDefinitionLogShippingLogGroup0E405BD0", + }, + "awslogs-region": "eu-west-1", + "awslogs-stream-prefix": "frontend/CODE/tag-page-rendering/devx-logs-sidecar", + }, + }, + "MountPoints": [ + { + "ContainerPath": "/init", + "ReadOnly": false, + "SourceVolume": "logging-volume", + }, + ], + "Name": "LogShipping", + "ReadonlyRootFilesystem": true, + "VersionConsistency": "disabled", + }, + ], + "Cpu": "1024", + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "EcsTaskDefinitionExecutionRoleBE450C73", + "Arn", + ], + }, + "Family": "TagPageRenderingCODEEcsTaskDefinition616FF027", + "Memory": "2048", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE", + ], + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "EcsTaskDefinitionTaskRoleB7B6D8DD", + "Arn", + ], + }, + "Volumes": [ + { + "Name": "logging-volume", + }, + ], + }, + "Type": "AWS::ECS::TaskDefinition", + }, + "EcsTaskDefinitionExecutionRoleBE450C73": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "EcsTaskDefinitionExecutionRoleDefaultPolicy1611A942": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":ecr:eu-west-1:", + { + "Ref": "DeployToolsAccountIdParameter", + }, + ":repository/guardian/dotcom-rendering", + ], + ], + }, + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*", + }, + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ], + "Effect": "Allow", + "Resource": "arn:aws:ecr:eu-west-1:694911143906:repository/aws-guardduty-agent-fargate", + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "EcsTaskDefinitionLogShippingLogGroup0E405BD0", + "Arn", + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "EcsTaskDefinitionExecutionRoleDefaultPolicy1611A942", + "Roles": [ + { + "Ref": "EcsTaskDefinitionExecutionRoleBE450C73", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "EcsTaskDefinitionLogShippingLogGroup0E405BD0": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 1, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "EcsTaskDefinitionTaskRoleB7B6D8DD": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "GetDistributablePolicyTagpagerendering346128E3": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "DistributionBucketName", + }, + "/frontend/CODE/tag-page-rendering/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GetDistributablePolicyTagpagerendering346128E3", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE": { + "Properties": { + "GroupDescription": "Allow all outbound HTTPS traffic", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound HTTPS traffic", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443, + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36": { + "Properties": { + "GroupDescription": "Allow all outbound HTTPS traffic", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound HTTPS traffic", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443, + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering-ecs", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingecsfromTagPageRenderingCODEInternalIngressSecurityGroupTagpagerendering0B98E4F990001A2DADD1": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingecsfromTagPageRenderingCODELoadBalancerTagpagerenderingSecurityGroup301E895290004324C739": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingfromTagPageRenderingCODEInternalIngressSecurityGroupTagpagerendering0B98E4F9900035DE48AC": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuHttpsEgressSecurityGroupTagpagerenderingfromTagPageRenderingCODELoadBalancerTagpagerenderingSecurityGroup301E895290002304A373": { + "Properties": { + "Description": "Load balancer to target", + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupIngress", + }, + "GuLogShippingPolicy981BFE5A": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kinesis:Describe*", + "kinesis:Put*", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:kinesis:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":stream/", + { + "Ref": "LoggingStreamName", + }, + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "GuLogShippingPolicy981BFE5A", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "InstanceRoleTagpagerendering171BC2F9": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Path": "/", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "InternalIngressSecurityGroupTagpagerendering38FD16DB": { + "Properties": { + "GroupDescription": "Allow restricted ingress from CIDR ranges", + "SecurityGroupIngress": [ + { + "CidrIp": "10.0.0.0/8", + "Description": "Allow access on port 443 from 10.0.0.0/8", + "FromPort": 443, + "IpProtocol": "tcp", + "ToPort": 443, + }, + ], + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "InternalIngressSecurityGroupTagpagerenderingtoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF3890004F95C890": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "InternalIngressSecurityGroupTagpagerenderingtoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900094544771": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "ListenerTagpagerendering92E57078": { + "Properties": { + "Certificates": [ + { + "CertificateArn": { + "Ref": "CertificateTagpagerendering5182FE63", + }, + }, + ], + "DefaultActions": [ + { + "ForwardConfig": { + "TargetGroups": [ + { + "TargetGroupArn": { + "Ref": "TargetGroupTagpagerendering42E428EE", + }, + "Weight": 1, + }, + { + "TargetGroupArn": { + "Ref": "EcsTargetGroupTagpagerendering06213654", + }, + "Weight": 0, + }, + ], + }, + "Type": "forward", + }, + ], + "LoadBalancerArn": { + "Ref": "LoadBalancerTagpagerenderingB0B7AC4E", + }, + "Port": 443, + "Protocol": "HTTPS", + "SslPolicy": "ELBSecurityPolicy-TLS13-1-2-2021-06", + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener", + }, + "LoadBalancerDNS": { + "Properties": { + "Name": "tag-page-rendering.code.dev-guardianapis.com", + "RecordType": "CNAME", + "ResourceRecords": [ + { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingB0B7AC4E", + "DNSName", + ], + }, + ], + "Stage": "CODE", + "TTL": 3600, + }, + "Type": "Guardian::DNS::RecordSet", + }, + "LoadBalancerTagpagerenderingB0B7AC4E": { + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "true", + }, + { + "Key": "routing.http.x_amzn_tls_version_and_cipher_suite.enabled", + "Value": "true", + }, + { + "Key": "routing.http.drop_invalid_header_fields.enabled", + "Value": "true", + }, + { + "Key": "access_logs.s3.enabled", + "Value": "true", + }, + { + "Key": "access_logs.s3.bucket", + "Value": { + "Ref": "AccessLoggingBucket", + }, + }, + { + "Key": "access_logs.s3.prefix", + "Value": "application-load-balancer/CODE/frontend/tag-page-rendering", + }, + { + "Key": "idle_timeout.timeout_seconds", + "Value": "4", + }, + ], + "Scheme": "internal", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + { + "Fn::GetAtt": [ + "InternalIngressSecurityGroupTagpagerendering38FD16DB", + "GroupId", + ], + }, + ], + "Subnets": { + "Ref": "tagpagerenderingPrivateSubnets", + }, + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "Type": "application", + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + }, + "LoadBalancerTagpagerenderingSecurityGroup4D481A16": { + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB TagPageRenderingCODELoadBalancerTagpagerenderingC39A1127", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::EC2::SecurityGroup", + }, + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerendering51E7AF38900020A4AE4F": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "LoadBalancerTagpagerenderingSecurityGrouptoTagPageRenderingCODEGuHttpsEgressSecurityGroupTagpagerenderingecsBADC1D1E900047DE752F": { + "Properties": { + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingecsF0505C36", + "GroupId", + ], + }, + "FromPort": 9000, + "GroupId": { + "Fn::GetAtt": [ + "LoadBalancerTagpagerenderingSecurityGroup4D481A16", + "GroupId", + ], + }, + "IpProtocol": "tcp", + "ToPort": 9000, + }, + "Type": "AWS::EC2::SecurityGroupEgress", + }, + "ParameterStoreReadTagpagerendering4CA7B6E3": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ssm:GetParametersByPath", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/CODE/frontend/tag-page-rendering", + ], + ], + }, + }, + { + "Action": [ + "ssm:GetParameters", + "ssm:GetParameter", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:ssm:eu-west-1:", + { + "Ref": "AWS::AccountId", + }, + ":parameter/CODE/frontend/tag-page-rendering/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "parameter-store-read-policy", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + { + "Ref": "EcsTaskDefinitionTaskRoleB7B6D8DD", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "RenderingBaseURLParamAE11E777": { + "Properties": { + "Description": "The rendering base URL for frontend to call the tag-page-rendering app in the CODE environment", + "Name": "/frontend/code/tag-page-rendering.baseURL", + "Tags": { + "Stack": "frontend", + "Stage": "CODE", + "gu:cdk:version": "TEST", + "gu:repo": "guardian/dotcom-rendering", + }, + "Type": "String", + "Value": "https://tag-page-rendering.code.dev-guardianapis.com", + }, + "Type": "AWS::SSM::Parameter", + }, + "SsmSshPolicy4CFC977E": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ec2messages:AcknowledgeMessage", + "ec2messages:DeleteMessage", + "ec2messages:FailMessage", + "ec2messages:GetEndpoint", + "ec2messages:GetMessages", + "ec2messages:SendReply", + "ssm:UpdateInstanceInformation", + "ssm:ListInstanceAssociations", + "ssm:DescribeInstanceProperties", + "ssm:DescribeDocumentParameters", + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel", + ], + "Effect": "Allow", + "Resource": "*", + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "ssm-ssh-policy", + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "TargetGroupTagpagerendering42E428EE": { + "Properties": { + "HealthCheckIntervalSeconds": 10, + "HealthCheckPath": "/_healthcheck", + "HealthCheckProtocol": "HTTP", + "HealthCheckTimeoutSeconds": 5, + "HealthyThresholdCount": 5, + "Port": 9000, + "Protocol": "HTTP", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + "TargetGroupAttributes": [ + { + "Key": "deregistration_delay.timeout_seconds", + "Value": "30", + }, + { + "Key": "stickiness.enabled", + "Value": "false", + }, + ], + "TargetType": "instance", + "UnhealthyThresholdCount": 2, + "VpcId": { + "Ref": "VpcId", + }, + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + }, + "frontendCODEtagpagerendering3C218821": { + "DependsOn": [ + "InstanceRoleTagpagerendering171BC2F9", + ], + "Properties": { + "LaunchTemplateData": { + "IamInstanceProfile": { + "Arn": { + "Fn::GetAtt": [ + "frontendCODEtagpagerenderingProfile70961F87", + "Arn", + ], + }, + }, + "ImageId": { + "Ref": "AMITagpagerendering", + }, + "InstanceType": "t4g.small", + "MetadataOptions": { + "HttpTokens": "required", + "InstanceMetadataTags": "enabled", + }, + "Monitoring": { + "Enabled": true, + }, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "GuHttpsEgressSecurityGroupTagpagerenderingDC233ADE", + "GroupId", + ], + }, + ], + "TagSpecifications": [ + { + "ResourceType": "instance", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-CODE/frontend-CODE-tag-page-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + { + "ResourceType": "volume", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-CODE/frontend-CODE-tag-page-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash +set -ev +groupadd frontend +useradd -r -m -s /usr/bin/nologin -g frontend dotcom-rendering +cd /home/dotcom-rendering +aws --region eu-west-1 s3 cp s3://", + { + "Ref": "DistributionBucketName", + }, + "/frontend/CODE/tag-page-rendering/tag-page-rendering.tar.gz ./ +tar -zxf tag-page-rendering.tar.gz tag-page-rendering +chown -R dotcom-rendering:frontend tag-page-rendering +cd tag-page-rendering +mkdir /var/log/dotcom-rendering +chown -R dotcom-rendering:frontend /var/log/dotcom-rendering +cat > /etc/systemd/system/tag-page-rendering.service << EOF +[Unit] +Description=tag-page-rendering +After=network.target +[Service] +WorkingDirectory=/home/dotcom-rendering/tag-page-rendering +Type=simple +User=dotcom-rendering +Group=frontend +StandardError=journal +StandardOutput=journal +Environment=TERM=xterm-256color +Environment=NODE_ENV=production +Environment=GU_STAGE=CODE +Environment=GU_APP=tag-page-rendering +Environment=GU_STACK=frontend +ExecStart=make prod +Restart=on-failure +[Install] +WantedBy=multi-user.target +EOF +systemctl enable tag-page-rendering +systemctl start tag-page-rendering", + ], + ], + }, + }, + }, + "TagSpecifications": [ + { + "ResourceType": "launch-template", + "Tags": [ + { + "Key": "App", + "Value": "tag-page-rendering", + }, + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Name", + "Value": "TagPageRendering-CODE/frontend-CODE-tag-page-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + ], + }, + "Type": "AWS::EC2::LaunchTemplate", + }, + "frontendCODEtagpagerenderingProfile70961F87": { + "Properties": { + "Roles": [ + { + "Ref": "InstanceRoleTagpagerendering171BC2F9", + }, + ], + }, + "Type": "AWS::IAM::InstanceProfile", + }, + "tagpagerenderingEcsClusterE7696595": { + "Properties": { + "Tags": [ + { + "Key": "gu:cdk:version", + "Value": "TEST", + }, + { + "Key": "gu:repo", + "Value": "guardian/dotcom-rendering", + }, + { + "Key": "Stack", + "Value": "frontend", + }, + { + "Key": "Stage", + "Value": "CODE", + }, + ], + }, + "Type": "AWS::ECS::Cluster", + }, + }, +} +`; diff --git a/dotcom-rendering/cdk/lib/renderingStack.test.ts b/dotcom-rendering/cdk/lib/renderingStack.test.ts index 88a53603d71..c55a7f4a421 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.test.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.test.ts @@ -1,6 +1,7 @@ import { App } from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; import { InstanceClass, InstanceSize, InstanceType } from 'aws-cdk-lib/aws-ec2'; +import { TagPageRenderingPropsCODE } from '../bin/cdk'; import { RenderingCDKStack } from './renderingStack'; /** @@ -51,4 +52,15 @@ describe('The RenderingCDKStack', () => { const template = Template.fromStack(stack); expect(template.toJSON()).toMatchSnapshot(); }); + + it('matches the snapshot for Tag Page Rendering CODE (uses ECS)', () => { + const app = new App(); + + const stack = new RenderingCDKStack(app, 'TagPageRendering-CODE', { + ...TagPageRenderingPropsCODE, + imageIdentifier: 'sha256:12345', + }); + const template = Template.fromStack(stack); + expect(template.toJSON()).toMatchSnapshot(); + }); }); diff --git a/dotcom-rendering/cdk/lib/renderingStack.ts b/dotcom-rendering/cdk/lib/renderingStack.ts index 8d5184affb5..f436001ba02 100644 --- a/dotcom-rendering/cdk/lib/renderingStack.ts +++ b/dotcom-rendering/cdk/lib/renderingStack.ts @@ -1,4 +1,4 @@ -import { type Alarms, GuEc2App } from '@guardian/cdk'; +import type { Alarms } from '@guardian/cdk'; import { AccessScope } from '@guardian/cdk/lib/constants'; import type { NoMonitoring } from '@guardian/cdk/lib/constructs/cloudwatch'; import type { GuStackProps } from '@guardian/cdk/lib/constructs/core'; @@ -8,6 +8,7 @@ import { } from '@guardian/cdk/lib/constructs/core'; import { GuCname } from '@guardian/cdk/lib/constructs/dns/dns-records'; import { GuAllowPolicy } from '@guardian/cdk/lib/constructs/iam'; +import { GuLoadBalancedAppExperimental } from '@guardian/cdk/lib/experimental/patterns/gu-load-balanced-app'; import type { GuAsgCapacity } from '@guardian/cdk/lib/types'; import { aws_cloudwatch, type App as CDKApp, Duration } from 'aws-cdk-lib'; import type { ScalingInterval } from 'aws-cdk-lib/aws-applicationautoscaling'; @@ -37,11 +38,21 @@ export interface RenderingCDKStackProps extends Omit { }; }; }; + + /** + * Which image to run. + * This should be the image digest (e.g. 'sha256:abc123') to ensure immutable deployments. + * + * @note Currently optional to control which services run in an EC2-ECS hybrid mode, or EC2-only. + * + * @see https://docs.docker.com/dhi/core-concepts/digests + */ + imageIdentifier?: string; } const addCPUStepScalingPolicy = ( context: RenderingCDKStack, - ec2App: GuEc2App, + app: GuLoadBalancedAppExperimental, props: RenderingCDKStackProps, stage: string, ) => { @@ -52,7 +63,7 @@ const addCPUStepScalingPolicy = ( unit: Unit.PERCENT, dimensionsMap: { AutoScalingGroupName: - ec2App.autoScalingGroup.autoScalingGroupName, + app.autoScalingGroup!.autoScalingGroupName, }, statistic: aws_cloudwatch.Stats.percentile(90), period: Duration.seconds(30), @@ -78,7 +89,7 @@ const addCPUStepScalingPolicy = ( context, 'CPUScaleUpPolicy', { - autoScalingGroup: ec2App.autoScalingGroup, + autoScalingGroup: app.autoScalingGroup!, metric: cpuMetric, scalingSteps: props.scaling.policies.step.cpu.scalingStepsOut, adjustmentType: AdjustmentType.PERCENT_CHANGE_IN_CAPACITY, @@ -101,15 +112,15 @@ const addCPUStepScalingPolicy = ( const addLatencyStepScalingPolicy = ( context: RenderingCDKStack, - ec2App: GuEc2App, + app: GuLoadBalancedAppExperimental, props: RenderingCDKStackProps, stage: string, ) => { if (stage === 'PROD' && props.scaling.policies?.step?.latency) { const latencyMetric = new Metric({ dimensionsMap: { - LoadBalancer: ec2App.loadBalancer.loadBalancerFullName, - TargetGroup: ec2App.targetGroup.targetGroupFullName, + LoadBalancer: app.loadBalancer.loadBalancerFullName, + TargetGroup: app.targetGroups.ec2!.targetGroupFullName, }, metricName: 'TargetResponseTime', namespace: 'AWS/ApplicationELB', @@ -137,7 +148,7 @@ const addLatencyStepScalingPolicy = ( context, 'LatencyScaleUpPolicy', { - autoScalingGroup: ec2App.autoScalingGroup, + autoScalingGroup: app.autoScalingGroup!, metric: latencyMetric, scalingSteps: props.scaling.policies.step.latency.scalingStepsOut, @@ -162,7 +173,7 @@ const addLatencyStepScalingPolicy = ( /** Scale in policy */ new StepScalingPolicy(context, 'LatencyScaleDownPolicy', { - autoScalingGroup: ec2App.autoScalingGroup, + autoScalingGroup: app.autoScalingGroup!, metric: latencyMetric, scalingSteps: props.scaling.policies.step.latency.scalingStepsIn, adjustmentType: AdjustmentType.CHANGE_IN_CAPACITY, @@ -183,7 +194,14 @@ export class RenderingCDKStack extends CDKStack { }); const { stack: guStack, region, account } = this; - const { guApp, stage, instanceType, scaling, domainName } = props; + const { + guApp, + stage, + instanceType, + scaling, + domainName, + imageIdentifier, + } = props; const artifactsBucket = GuDistributionBucketParameter.getInstance(this).valueAsString; @@ -201,61 +219,98 @@ export class RenderingCDKStack extends CDKStack { } satisfies Alarms) : ({ noMonitoring: true } satisfies NoMonitoring); - const ec2App = new GuEc2App(this, { + const app = new GuLoadBalancedAppExperimental(this, { app: guApp, access: { // Restrict access to this range within the VPC cidrRanges: [Peer.ipv4('10.0.0.0/8')], scope: AccessScope.INTERNAL, }, - instanceMetricGranularity: '1Minute', - applicationLogging: { - enabled: true, - systemdUnitName: guApp, - }, // TODO - should we change to 3000? applicationPort: 9000, // Certificate is necessary for the creation of a listener on port 443, // instead of the default 8080 which is unreachable. certificateProps: { domainName }, healthcheck: { path: '/_healthcheck' }, - instanceType, monitoringConfiguration, - roleConfiguration: { - additionalPolicies: [ - new GuAllowPolicy(this, 'AllowPolicyCloudwatchLogs', { - actions: ['cloudwatch:*', 'logs:*'], - resources: ['*'], - }), - new GuAllowPolicy(this, 'AllowPolicyDescribeDecryptKms', { - actions: ['kms:Decrypt', 'kms:DescribeKey'], - resources: [ - `arn:aws:kms:${region}:${account}:FrontendConfigKey`, - ], - }), - new GuAllowPolicy(this, 'AllowPolicyGetSsmParamsByPath', { - actions: [ - 'ssm:GetParametersByPath', - 'ssm:GetParameter', - ], - resources: [ - // This is for backwards compatibility reasons with frontend apps and an old SSM naming system - // TODO - ideally we should convert these params to use the newer naming style for consistency - `arn:aws:ssm:${region}:${this.account}:parameter/frontend/*`, - `arn:aws:ssm:${region}:${this.account}:parameter/dotcom/*`, - ], - }), - ], + additionalPolicies: [ + new GuAllowPolicy(this, 'AllowPolicyCloudwatchLogs', { + actions: ['cloudwatch:*', 'logs:*'], + resources: ['*'], + }), + new GuAllowPolicy(this, 'AllowPolicyDescribeDecryptKms', { + actions: ['kms:Decrypt', 'kms:DescribeKey'], + resources: [ + `arn:aws:kms:${region}:${account}:FrontendConfigKey`, + ], + }), + new GuAllowPolicy(this, 'AllowPolicyGetSsmParamsByPath', { + actions: ['ssm:GetParametersByPath', 'ssm:GetParameter'], + resources: [ + // This is for backwards compatibility reasons with frontend apps and an old SSM naming system + // TODO - ideally we should convert these params to use the newer naming style for consistency + `arn:aws:ssm:${region}:${this.account}:parameter/frontend/*`, + `arn:aws:ssm:${region}:${this.account}:parameter/dotcom/*`, + ], + }), + ], + ec2Props: { + instanceMetricGranularity: '1Minute', + applicationLogging: { + enabled: true, + systemdUnitName: guApp, + }, + instanceType, + scaling, + userData: getUserData({ + guApp, + guStack, + stage, + artifactsBucket, + }), }, - scaling, - userData: getUserData({ - guApp, - guStack, - stage, - artifactsBucket, - }), + + // Provision ECS resources only when `imageIdentifier` has been provided + ...(imageIdentifier == null + ? {} + : { + ecsProps: { + repositoryName: 'guardian/dotcom-rendering', + imageIdentifier, + + // TODO tune these values + memoryLimitMiB: 2048, + cpu: 1024, + scaling: { + minimumTasks: 1, + maximumTasks: 2, + }, + }, + + // Route all traffic to EC2 + targetGroupWeights: { + ec2: 1, + ecs: 0, + }, + }), }); + if (imageIdentifier != null) { + const ecsEnvVars: Record = { + NODE_ENV: 'production', + GU_STAGE: stage, + GU_APP: guApp, + GU_STACK: guStack, + }; + + for (const [key, value] of Object.entries(ecsEnvVars)) { + app.ecsService?.taskDefinition.defaultContainer?.addEnvironment( + key, + value, + ); + } + } + /** * The default Node server keep alive timeout is 5 seconds * @see https://nodejs.org/api/http.html#serverkeepalivetimeout @@ -264,21 +319,21 @@ export class RenderingCDKStack extends CDKStack { * so that the Node app does not prematurely close the connection before the load balancer can accept the response. * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout */ - ec2App.loadBalancer.setAttribute('idle_timeout.timeout_seconds', '4'); + app.loadBalancer.setAttribute('idle_timeout.timeout_seconds', '4'); // Maps the certificate domain name to the load balancer DNS name new GuCname(this, 'LoadBalancerDNS', { domainName, app: guApp, - resourceRecord: ec2App.loadBalancer.loadBalancerDnsName, + resourceRecord: app.loadBalancer.loadBalancerDnsName, ttl: Duration.hours(1), }); /** Add CPU utilisation based STEP scaling policy for PROD only if a policy is defined */ - addCPUStepScalingPolicy(this, ec2App, props, stage); + addCPUStepScalingPolicy(this, app, props, stage); /** Add latency-based STEP scaling policy for PROD only if a policy is defined */ - addLatencyStepScalingPolicy(this, ec2App, props, stage); + addLatencyStepScalingPolicy(this, app, props, stage); // Saves the value of the rendering base URL to SSM for frontend apps to use new StringParameter(this, 'RenderingBaseURLParam', { diff --git a/dotcom-rendering/docs/contributing/how-to.md b/dotcom-rendering/docs/contributing/how-to.md index e1505acda0a..9448b127e12 100644 --- a/dotcom-rendering/docs/contributing/how-to.md +++ b/dotcom-rendering/docs/contributing/how-to.md @@ -49,3 +49,13 @@ We currently use [SWR](https://swr.vercel.app/) to manage AJAX requests on the client. Your starting point should be the [`useApi` hook](/dotcom-rendering/src/lib/useApi.ts), which is DCR's wrapper around SWR's own hook. Because this hook requires client-side React code, you will need to place your component in an `` wrapper. + +## How can I interact with the API on CODE/PROD? + +The DCR load balancers are configured to accept requests from within the VPC; they are not available on the public internet. +To make requests to the API from your local machine, use the [SOCKS proxy](https://github.com/guardian/platform/blob/main/cdk/lib/socks-proxy.ts): + +1. Obtain AWS credentials via Janus, preferably using the minimal `socks-proxy` Developer Policy (`DeveloperPolicyGrants.frontendSocksProxy`) +2. Run the `browse-with-socks-proxy` script from the [`guardian/platform` repo](https://github.com/guardian/platform/blob/main/scripts/browse-with-socks-proxy): + This will start a new instance of the Chrome browser with a SOCKS proxy configured to route request via an EC2 instance in the VPC. + You can now browse the DCR API on CODE/PROD from this browser instance. diff --git a/dotcom-rendering/fixtures/config.js b/dotcom-rendering/fixtures/config.js index 8ee0d539cf7..a5dd4b7b174 100644 --- a/dotcom-rendering/fixtures/config.js +++ b/dotcom-rendering/fixtures/config.js @@ -155,7 +155,6 @@ module.exports = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/cricket-match.ts b/dotcom-rendering/fixtures/generated/cricket-match.ts index b98dad62453..9fad68405be 100644 --- a/dotcom-rendering/fixtures/generated/cricket-match.ts +++ b/dotcom-rendering/fixtures/generated/cricket-match.ts @@ -21,654 +21,655 @@ export const cricketMatchData: FECricketMatchPage = { id: 'a359844f-fc07-9cfa-d4cc-9a9ac0d5d075', home: true, lineup: [ + 'Emilio Gay', 'Ben Duckett', - 'Zak Crawley', - 'Ollie Pope', + 'Jacob Bethell', 'Joe Root', 'Harry Brook', - 'Jacob Bethell', 'Jamie Smith', - 'Chris Woakes', - 'Jamie Overton', + 'Ben Stokes', 'Gus Atkinson', + 'Ollie Robinson', 'Josh Tongue', + 'Shoaib Bashir', ], + teamTagId: 'sport/england-cricket-team', }, { - name: 'India', - id: 'f822b9f9-9fdc-399f-54f9-e621edaf0a28', + name: 'New Zealand', + id: '110c70b5-c05f-3be7-6670-baecd50a8c6b', home: false, lineup: [ - 'Yashasvi Jaiswal', - 'Lokesh Rahul', - 'Sai Sudharsan', - 'Shubman Gill', - 'Karun Nair', - 'Dhruv Jurel', - 'Ravindra Jadeja', - 'Washington Sundar', - 'Akash Deep', - 'Prasidh Krishna', - 'Mohammed Siraj', + 'Tom Latham', + 'Devon Conway', + 'Kane Williamson', + 'Rachin Ravindra', + 'Daryl Mitchell', + 'Tom Blundell', + 'Glenn Phillips', + 'Nathan Smith', + 'Kyle Jamieson', + 'Matt Henry', + "Will O'Rourke", ], + teamTagId: 'sport/new-zealand-cricket-team', }, ], innings: [ { order: 1, - battingTeam: 'India', - runsScored: 224, - overs: '69.4', + battingTeam: 'England', + runsScored: 140, + wickets: 10, + overs: '39.4', declared: false, forfeited: false, - description: 'India first innings', + description: 'England first innings', batters: [ { - name: 'Yashasvi Jaiswal', + name: 'Ben Duckett', order: 1, - ballsFaced: 9, - runs: 2, - fours: 0, + ballsFaced: 37, + runs: 19, + fours: 2, sixes: 0, out: true, - howOut: 'lbw b Atkinson', + howOut: 'lbw b Smith', onStrike: false, nonStrike: true, }, { - name: 'Lokesh Rahul', + name: 'Emilio Gay', order: 2, - ballsFaced: 40, - runs: 14, - fours: 1, + ballsFaced: 14, + runs: 8, + fours: 2, sixes: 0, out: true, - howOut: 'b Woakes', + howOut: 'c Mitchell b Jamieson', onStrike: false, nonStrike: true, }, { - name: 'Sai Sudharsan', + name: 'Jacob Bethell', order: 3, - ballsFaced: 108, - runs: 38, - fours: 6, + ballsFaced: 22, + runs: 6, + fours: 0, sixes: 0, out: true, - howOut: 'c Smith b Tongue', + howOut: "lbw b O'Rourke", onStrike: false, nonStrike: true, }, { - name: 'Shubman Gill', + name: 'Joe Root', order: 4, - ballsFaced: 35, - runs: 21, - fours: 4, + ballsFaced: 8, + runs: 1, + fours: 0, sixes: 0, out: true, - howOut: 'Run Out Atkinson', + howOut: "c Blundell b O'Rourke", onStrike: false, nonStrike: true, }, { - name: 'Karun Nair', + name: 'Harry Brook', order: 5, - ballsFaced: 109, - runs: 57, - fours: 8, + ballsFaced: 71, + runs: 56, + fours: 10, sixes: 0, out: true, - howOut: 'lbw b Tongue', + howOut: 'c Jamieson b Smith', onStrike: false, nonStrike: true, }, { - name: 'Ravindra Jadeja', + name: 'Jamie Smith', order: 6, - ballsFaced: 13, - runs: 9, - fours: 1, + ballsFaced: 6, + runs: 1, + fours: 0, sixes: 0, out: true, - howOut: 'c Smith b Tongue', + howOut: 'b Jamieson', onStrike: false, nonStrike: true, }, { - name: 'Dhruv Jurel', + name: 'Ben Stokes', order: 7, - ballsFaced: 40, - runs: 19, + ballsFaced: 23, + runs: 12, fours: 2, sixes: 0, out: true, - howOut: 'c Brook b Atkinson', + howOut: 'c Williamson b Jamieson', onStrike: false, nonStrike: true, }, { - name: 'Washington Sundar', + name: 'Gus Atkinson', order: 8, - ballsFaced: 55, - runs: 26, - fours: 3, + ballsFaced: 11, + runs: 4, + fours: 0, sixes: 0, out: true, - howOut: 'c Overton b Atkinson', + howOut: 'lbw b Jamieson', onStrike: false, nonStrike: true, }, { - name: 'Akash Deep', + name: 'Ollie Robinson', order: 9, - ballsFaced: 7, - runs: 0, + ballsFaced: 5, + runs: 1, fours: 0, sixes: 0, - out: false, - howOut: 'Not Out', + out: true, + howOut: 'c Blundell b Jamieson', onStrike: false, - nonStrike: false, + nonStrike: true, }, { - name: 'Mohammed Siraj', + name: 'Josh Tongue', order: 10, - ballsFaced: 4, - runs: 0, - fours: 0, + ballsFaced: 23, + runs: 10, + fours: 2, sixes: 0, - out: true, - howOut: 'b Atkinson', + out: false, + howOut: 'Not Out', onStrike: false, - nonStrike: false, + nonStrike: true, }, { - name: 'Prasidh Krishna', + name: 'Shoaib Bashir', order: 11, - ballsFaced: 2, - runs: 0, - fours: 0, + ballsFaced: 19, + runs: 14, + fours: 1, sixes: 0, out: true, - howOut: 'c Smith b Atkinson', + howOut: 'c Williamson b Smith', onStrike: false, - nonStrike: false, + nonStrike: true, }, ], bowlers: [ { - name: 'Chris Woakes', + name: 'Matt Henry', order: 1, - overs: 14, + overs: 4, maidens: 1, - runs: 46, - wickets: 1, - balls: 84, + runs: 8, + wickets: 0, + balls: 24, }, { - name: 'Gus Atkinson', + name: 'Kyle Jamieson', order: 2, - overs: 21, - maidens: 8, - runs: 33, + overs: 14, + maidens: 0, + runs: 62, wickets: 5, - balls: 130, + balls: 84, }, { - name: 'Josh Tongue', + name: 'Nathan Smith', order: 3, - overs: 16, - maidens: 4, - runs: 57, + overs: 10, + maidens: 1, + runs: 38, wickets: 3, - balls: 96, + balls: 64, }, { - name: 'Jamie Overton', + name: "Will O'Rourke", order: 4, - overs: 16, - maidens: 0, - runs: 66, - wickets: 0, - balls: 96, - }, - { - name: 'Jacob Bethell', - order: 5, - overs: 2, - maidens: 1, - runs: 4, - wickets: 0, - balls: 12, + overs: 11, + maidens: 3, + runs: 25, + wickets: 2, + balls: 66, }, ], fallOfWicket: [ { order: 1, - name: 'Yashasvi Jaiswal', - runs: 10, + name: 'Emilio Gay', + runs: 16, }, { order: 2, - name: 'Lokesh Rahul', - runs: 38, + name: 'Ben Duckett', + runs: 31, }, { order: 3, - name: 'Shubman Gill', - runs: 83, + name: 'Jacob Bethell', + runs: 33, }, { order: 4, - name: 'Sai Sudharsan', - runs: 101, + name: 'Joe Root', + runs: 34, }, { order: 5, - name: 'Ravindra Jadeja', - runs: 123, + name: 'Jamie Smith', + runs: 55, }, { order: 6, - name: 'Dhruv Jurel', - runs: 153, + name: 'Ben Stokes', + runs: 94, }, { order: 7, - name: 'Karun Nair', - runs: 218, + name: 'Gus Atkinson', + runs: 108, }, { order: 8, - name: 'Washington Sundar', - runs: 220, + name: 'Harry Brook', + runs: 113, }, { order: 9, - name: 'Mohammed Siraj', - runs: 224, + name: 'Ollie Robinson', + runs: 118, }, { order: 10, - name: 'Prasidh Krishna', - runs: 224, + name: 'Shoaib Bashir', + runs: 140, }, ], - byes: 12, - legByes: 6, - noBalls: 4, + byes: 6, + legByes: 1, + noBalls: 1, penalties: 0, - wides: 16, - extras: 38, + wides: 0, + extras: 8, }, { order: 2, - battingTeam: 'England', - runsScored: 247, - overs: '51.2', + battingTeam: 'New Zealand', + runsScored: 113, + wickets: 10, + overs: '29.5', declared: false, forfeited: false, - description: 'England first innings', + description: 'New Zealand first innings', batters: [ { - name: 'Zak Crawley', + name: 'Tom Latham', order: 1, - ballsFaced: 57, - runs: 64, - fours: 14, + ballsFaced: 21, + runs: 3, + fours: 0, sixes: 0, out: true, - howOut: 'c Jadeja b Krishna', + howOut: 'lbw b Atkinson', onStrike: false, nonStrike: true, }, { - name: 'Ben Duckett', + name: 'Devon Conway', order: 2, - ballsFaced: 38, - runs: 43, - fours: 5, - sixes: 2, + ballsFaced: 5, + runs: 1, + fours: 0, + sixes: 0, out: true, - howOut: 'c Jurel b Deep', + howOut: 'lbw b Robinson', onStrike: false, nonStrike: true, }, { - name: 'Ollie Pope', + name: 'Kane Williamson', order: 3, - ballsFaced: 44, - runs: 22, - fours: 4, + ballsFaced: 2, + runs: 0, + fours: 0, sixes: 0, out: true, - howOut: 'lbw b Siraj', + howOut: 'c Gay b Robinson', onStrike: false, - nonStrike: true, + nonStrike: false, }, { - name: 'Joe Root', + name: 'Rachin Ravindra', order: 4, - ballsFaced: 45, - runs: 29, - fours: 6, + ballsFaced: 1, + runs: 0, + fours: 0, sixes: 0, out: true, - howOut: 'lbw b Siraj', + howOut: 'lbw b Robinson', onStrike: false, - nonStrike: true, + nonStrike: false, }, { - name: 'Harry Brook', + name: 'Daryl Mitchell', order: 5, - ballsFaced: 64, - runs: 53, - fours: 5, - sixes: 1, + ballsFaced: 23, + runs: 12, + fours: 1, + sixes: 0, out: true, - howOut: 'b Siraj', + howOut: 'b Robinson', onStrike: false, nonStrike: true, }, { - name: 'Jacob Bethell', + name: 'Tom Blundell', order: 6, - ballsFaced: 14, - runs: 6, - fours: 1, + ballsFaced: 21, + runs: 4, + fours: 0, sixes: 0, out: true, - howOut: 'lbw b Siraj', + howOut: 'b Tongue', onStrike: false, nonStrike: true, }, { - name: 'Jamie Smith', + name: 'Glenn Phillips', order: 7, - ballsFaced: 22, - runs: 8, - fours: 1, + ballsFaced: 39, + runs: 34, + fours: 6, sixes: 0, out: true, - howOut: 'c Rahul b Krishna', + howOut: 'b Tongue', onStrike: false, nonStrike: true, }, { - name: 'Jamie Overton', + name: 'Nathan Smith', order: 8, - ballsFaced: 4, - runs: 0, - fours: 0, + ballsFaced: 18, + runs: 15, + fours: 2, sixes: 0, out: true, - howOut: 'lbw b Krishna', + howOut: 'b Tongue', onStrike: false, - nonStrike: false, + nonStrike: true, }, { - name: 'Gus Atkinson', + name: 'Kyle Jamieson', order: 9, - ballsFaced: 16, - runs: 11, + ballsFaced: 29, + runs: 38, fours: 2, - sixes: 0, - out: true, - howOut: 'c Deep b Krishna', + sixes: 3, + out: false, + howOut: 'Not Out', onStrike: false, nonStrike: true, }, { - name: 'Josh Tongue', + name: "Will O'Rourke", order: 10, - ballsFaced: 7, - runs: 0, + ballsFaced: 17, + runs: 1, fours: 0, sixes: 0, - out: false, - howOut: 'Not Out', + out: true, + howOut: 'c Brook b Atkinson', onStrike: false, - nonStrike: false, + nonStrike: true, }, { - name: 'Chris Woakes', + name: 'Matt Henry', order: 11, - ballsFaced: 0, + ballsFaced: 5, runs: 0, fours: 0, sixes: 0, out: true, - howOut: 'Absent Hurt', + howOut: 'b Robinson', onStrike: false, nonStrike: false, }, ], bowlers: [ { - name: 'Mohammed Siraj', + name: 'Gus Atkinson', order: 1, - overs: 16, - maidens: 1, - runs: 86, - wickets: 4, - balls: 98, + overs: 5, + maidens: 0, + runs: 9, + wickets: 2, + balls: 30, }, { - name: 'Akash Deep', + name: 'Ollie Robinson', order: 2, - overs: 17, - maidens: 0, - runs: 80, - wickets: 1, - balls: 102, + overs: 10, + maidens: 3, + runs: 39, + wickets: 5, + balls: 65, }, { - name: 'Prasidh Krishna', + name: 'Josh Tongue', order: 3, - overs: 16, - maidens: 1, - runs: 62, - wickets: 4, - balls: 96, + overs: 10, + maidens: 0, + runs: 40, + wickets: 3, + balls: 60, }, { - name: 'Ravindra Jadeja', + name: 'Ben Stokes', order: 4, - overs: 2, + overs: 4, maidens: 0, - runs: 11, + runs: 22, wickets: 0, - balls: 12, + balls: 24, }, ], fallOfWicket: [ { order: 1, - name: 'Ben Duckett', - runs: 92, + name: 'Devon Conway', + runs: 2, }, { order: 2, - name: 'Zak Crawley', - runs: 129, + name: 'Kane Williamson', + runs: 2, }, { order: 3, - name: 'Ollie Pope', - runs: 142, + name: 'Rachin Ravindra', + runs: 2, }, { order: 4, - name: 'Joe Root', - runs: 175, + name: 'Tom Latham', + runs: 12, }, { order: 5, - name: 'Jacob Bethell', - runs: 195, + name: 'Daryl Mitchell', + runs: 20, }, { order: 6, - name: 'Jamie Smith', - runs: 215, + name: 'Tom Blundell', + runs: 29, }, { order: 7, - name: 'Jamie Overton', - runs: 215, + name: 'Glenn Phillips', + runs: 65, }, { order: 8, - name: 'Gus Atkinson', - runs: 235, + name: 'Nathan Smith', + runs: 82, }, { order: 9, - name: 'Harry Brook', - runs: 247, + name: "Will O'Rourke", + runs: 108, + }, + { + order: 10, + name: 'Matt Henry', + runs: 113, }, ], - byes: 6, - legByes: 2, - noBalls: 3, + byes: 0, + legByes: 3, + noBalls: 2, penalties: 0, wides: 0, - extras: 11, + extras: 5, }, { order: 3, - battingTeam: 'India', - runsScored: 396, - overs: '88.0', + battingTeam: 'England', + runsScored: 226, + wickets: 10, + overs: '56.0', declared: false, forfeited: false, - description: 'India second innings', + description: 'England second innings', batters: [ { - name: 'Yashasvi Jaiswal', + name: 'Ben Duckett', order: 1, - ballsFaced: 164, - runs: 118, - fours: 14, - sixes: 2, + ballsFaced: 46, + runs: 33, + fours: 6, + sixes: 0, out: true, - howOut: 'c Overton b Tongue', + howOut: "c Phillips b O'Rourke", onStrike: false, nonStrike: true, }, { - name: 'Lokesh Rahul', + name: 'Emilio Gay', order: 2, - ballsFaced: 28, - runs: 7, - fours: 1, + ballsFaced: 95, + runs: 57, + fours: 8, sixes: 0, out: true, - howOut: 'c Root b Tongue', + howOut: 'c Blundell b Smith', onStrike: false, nonStrike: true, }, { - name: 'Sai Sudharsan', + name: 'Jacob Bethell', order: 3, - ballsFaced: 29, - runs: 11, - fours: 1, + ballsFaced: 35, + runs: 14, + fours: 2, sixes: 0, out: true, - howOut: 'lbw b Atkinson', + howOut: 'b Henry', onStrike: false, nonStrike: true, }, { - name: 'Akash Deep', + name: 'Joe Root', order: 4, - ballsFaced: 94, - runs: 66, - fours: 12, + ballsFaced: 19, + runs: 8, + fours: 1, sixes: 0, out: true, - howOut: 'c Atkinson b Overton', + howOut: 'lbw b Smith', onStrike: false, nonStrike: true, }, { - name: 'Shubman Gill', + name: 'Harry Brook', order: 5, - ballsFaced: 9, - runs: 11, - fours: 2, + ballsFaced: 4, + runs: 0, + fours: 0, sixes: 0, out: true, - howOut: 'lbw b Atkinson', + howOut: "lbw b O'Rourke", onStrike: false, - nonStrike: true, + nonStrike: false, }, { - name: 'Karun Nair', + name: 'Jamie Smith', order: 6, - ballsFaced: 32, - runs: 17, - fours: 3, + ballsFaced: 52, + runs: 39, + fours: 6, sixes: 0, out: true, - howOut: 'c Smith b Atkinson', + howOut: 'b Smith', onStrike: false, nonStrike: true, }, { - name: 'Ravindra Jadeja', + name: 'Ben Stokes', order: 7, - ballsFaced: 77, - runs: 53, - fours: 5, + ballsFaced: 3, + runs: 0, + fours: 0, sixes: 0, out: true, - howOut: 'c Brook b Tongue', + howOut: 'b Smith', onStrike: false, - nonStrike: true, + nonStrike: false, }, { - name: 'Dhruv Jurel', + name: 'Gus Atkinson', order: 8, - ballsFaced: 46, - runs: 34, - fours: 4, + ballsFaced: 32, + runs: 14, + fours: 1, sixes: 0, out: true, - howOut: 'lbw b Overton', + howOut: 'c & b Jamieson', onStrike: false, nonStrike: true, }, { - name: 'Washington Sundar', + name: 'Ollie Robinson', order: 9, - ballsFaced: 46, - runs: 53, - fours: 4, - sixes: 4, + ballsFaced: 30, + runs: 29, + fours: 3, + sixes: 0, out: true, - howOut: 'c Crawley b Tongue', - onStrike: false, + howOut: 'c Phillips b Smith', + onStrike: true, nonStrike: true, }, { - name: 'Mohammed Siraj', + name: 'Josh Tongue', order: 10, - ballsFaced: 3, - runs: 0, - fours: 0, + ballsFaced: 13, + runs: 5, + fours: 1, sixes: 0, out: true, - howOut: 'lbw b Tongue', + howOut: 'b Smith', onStrike: false, - nonStrike: false, + nonStrike: true, }, { - name: 'Prasidh Krishna', + name: 'Shoaib Bashir', order: 11, - ballsFaced: 2, + ballsFaced: 8, runs: 0, fours: 0, sixes: 0, @@ -680,371 +681,366 @@ export const cricketMatchData: FECricketMatchPage = { ], bowlers: [ { - name: 'Gus Atkinson', + name: 'Kyle Jamieson', order: 1, - overs: 27, - maidens: 3, - runs: 127, - wickets: 3, - balls: 162, + overs: 12, + maidens: 2, + runs: 41, + wickets: 1, + balls: 72, }, { - name: 'Josh Tongue', + name: 'Nathan Smith', order: 2, - overs: 30, + overs: 17, maidens: 4, - runs: 125, - wickets: 5, - balls: 180, + runs: 70, + wickets: 6, + balls: 102, }, { - name: 'Jamie Overton', + name: "Will O'Rourke", order: 3, - overs: 22, - maidens: 2, - runs: 98, + overs: 16, + maidens: 4, + runs: 46, wickets: 2, - balls: 132, + balls: 96, }, { - name: 'Jacob Bethell', + name: 'Matt Henry', order: 4, - overs: 4, - maidens: 0, - runs: 13, - wickets: 0, - balls: 24, - }, - { - name: 'Joe Root', - order: 5, - overs: 5, + overs: 11, maidens: 1, - runs: 15, - wickets: 0, - balls: 30, + runs: 43, + wickets: 1, + balls: 66, }, ], fallOfWicket: [ { order: 1, - name: 'Lokesh Rahul', - runs: 46, + name: 'Ben Duckett', + runs: 52, }, { order: 2, - name: 'Sai Sudharsan', - runs: 70, + name: 'Jacob Bethell', + runs: 99, }, { order: 3, - name: 'Akash Deep', - runs: 177, + name: 'Emilio Gay', + runs: 126, }, { order: 4, - name: 'Shubman Gill', - runs: 189, + name: 'Harry Brook', + runs: 127, }, { order: 5, - name: 'Karun Nair', - runs: 229, + name: 'Joe Root', + runs: 127, }, { order: 6, - name: 'Yashasvi Jaiswal', - runs: 273, + name: 'Ben Stokes', + runs: 127, }, { order: 7, - name: 'Dhruv Jurel', - runs: 323, + name: 'Gus Atkinson', + runs: 184, }, { order: 8, - name: 'Ravindra Jadeja', - runs: 357, + name: 'Jamie Smith', + runs: 213, }, { order: 9, - name: 'Mohammed Siraj', - runs: 357, + name: 'Josh Tongue', + runs: 225, }, { order: 10, - name: 'Washington Sundar', - runs: 396, + name: 'Ollie Robinson', + runs: 226, }, ], - byes: 13, - legByes: 5, - noBalls: 2, + byes: 14, + legByes: 12, + noBalls: 1, penalties: 0, - wides: 6, - extras: 26, + wides: 0, + extras: 27, }, { order: 4, - battingTeam: 'England', - runsScored: 367, - overs: '85.1', + battingTeam: 'New Zealand', + runsScored: 138, + wickets: 10, + overs: '40.3', declared: false, forfeited: false, - description: 'England second innings', + description: 'New Zealand second innings', batters: [ { - name: 'Zak Crawley', + name: 'Tom Latham', order: 1, - ballsFaced: 36, - runs: 14, - fours: 2, + ballsFaced: 3, + runs: 0, + fours: 0, sixes: 0, out: true, - howOut: 'b Siraj', + howOut: 'c Brook b Atkinson', onStrike: false, - nonStrike: true, + nonStrike: false, }, { - name: 'Ben Duckett', + name: 'Devon Conway', order: 2, - ballsFaced: 83, - runs: 54, - fours: 6, + ballsFaced: 91, + runs: 41, + fours: 4, sixes: 0, out: true, - howOut: 'c Rahul b Krishna', + howOut: 'c Bethell b Stokes', onStrike: false, nonStrike: true, }, { - name: 'Ollie Pope', + name: 'Kane Williamson', order: 3, - ballsFaced: 34, - runs: 27, - fours: 5, + ballsFaced: 36, + runs: 18, + fours: 2, sixes: 0, out: true, - howOut: 'lbw b Siraj', + howOut: 'lbw b Tongue', onStrike: false, nonStrike: true, }, { - name: 'Joe Root', + name: "Will O'Rourke", order: 4, - ballsFaced: 152, - runs: 105, - fours: 12, + ballsFaced: 6, + runs: 0, + fours: 0, sixes: 0, out: true, - howOut: 'c Jurel b Krishna', + howOut: 'b Atkinson', onStrike: false, - nonStrike: true, + nonStrike: false, }, { - name: 'Harry Brook', + name: 'Rachin Ravindra', order: 5, - ballsFaced: 98, - runs: 111, - fours: 14, - sixes: 2, + ballsFaced: 21, + runs: 8, + fours: 1, + sixes: 0, out: true, - howOut: 'c Siraj b Deep', + howOut: 'b Robinson', onStrike: false, nonStrike: true, }, { - name: 'Jacob Bethell', + name: 'Daryl Mitchell', order: 6, - ballsFaced: 31, - runs: 5, - fours: 1, + ballsFaced: 3, + runs: 0, + fours: 0, sixes: 0, out: true, - howOut: 'b Krishna', + howOut: 'lbw b Robinson', onStrike: false, - nonStrike: true, + nonStrike: false, }, { - name: 'Jamie Smith', + name: 'Tom Blundell', order: 7, - ballsFaced: 20, - runs: 2, + ballsFaced: 12, + runs: 4, fours: 0, sixes: 0, out: true, - howOut: 'c Jurel b Siraj', + howOut: 'lbw b Tongue', onStrike: false, nonStrike: true, }, { - name: 'Jamie Overton', + name: 'Glenn Phillips', order: 8, - ballsFaced: 17, - runs: 9, - fours: 2, - sixes: 0, - out: true, - howOut: 'lbw b Siraj', + ballsFaced: 52, + runs: 44, + fours: 7, + sixes: 1, + out: false, + howOut: 'Not Out', onStrike: false, nonStrike: true, }, { - name: 'Gus Atkinson', + name: 'Nathan Smith', order: 9, - ballsFaced: 29, - runs: 17, - fours: 0, - sixes: 1, + ballsFaced: 3, + runs: 4, + fours: 1, + sixes: 0, out: true, - howOut: 'b Siraj', + howOut: 'c Smith b Atkinson', onStrike: false, nonStrike: true, }, { - name: 'Josh Tongue', + name: 'Kyle Jamieson', order: 10, - ballsFaced: 12, - runs: 0, - fours: 0, + ballsFaced: 9, + runs: 6, + fours: 1, sixes: 0, out: true, - howOut: 'b Krishna', + howOut: 'c Duckett b Atkinson', onStrike: false, - nonStrike: false, + nonStrike: true, }, { - name: 'Chris Woakes', + name: 'Matt Henry', order: 11, - ballsFaced: 0, + ballsFaced: 10, runs: 0, fours: 0, sixes: 0, - out: false, - howOut: 'Not Out', + out: true, + howOut: 'b Atkinson', onStrike: false, nonStrike: false, }, ], bowlers: [ { - name: 'Akash Deep', + name: 'Gus Atkinson', order: 1, - overs: 20, - maidens: 4, - runs: 85, - wickets: 1, - balls: 120, + overs: 11, + maidens: 3, + runs: 30, + wickets: 5, + balls: 69, }, { - name: 'Prasidh Krishna', + name: 'Ollie Robinson', order: 2, - overs: 27, - maidens: 3, - runs: 126, - wickets: 4, - balls: 162, + overs: 13, + maidens: 2, + runs: 38, + wickets: 2, + balls: 78, }, { - name: 'Mohammed Siraj', + name: 'Josh Tongue', order: 3, - overs: 30, - maidens: 6, - runs: 104, - wickets: 5, - balls: 181, + overs: 13, + maidens: 2, + runs: 48, + wickets: 2, + balls: 78, }, { - name: 'Washington Sundar', + name: 'Ben Stokes', order: 4, - overs: 4, - maidens: 0, - runs: 19, - wickets: 0, - balls: 24, - }, - { - name: 'Ravindra Jadeja', - order: 5, - overs: 4, + overs: 3, maidens: 0, - runs: 22, - wickets: 0, - balls: 24, + runs: 12, + wickets: 1, + balls: 18, }, ], fallOfWicket: [ { order: 1, - name: 'Zak Crawley', - runs: 50, + name: 'Tom Latham', + runs: 0, }, { order: 2, - name: 'Ben Duckett', - runs: 82, + name: 'Kane Williamson', + runs: 29, }, { order: 3, - name: 'Ollie Pope', - runs: 106, + name: "Will O'Rourke", + runs: 36, }, { order: 4, - name: 'Harry Brook', - runs: 301, + name: 'Rachin Ravindra', + runs: 53, }, { order: 5, - name: 'Jacob Bethell', - runs: 332, + name: 'Daryl Mitchell', + runs: 53, }, { order: 6, - name: 'Joe Root', - runs: 337, + name: 'Tom Blundell', + runs: 58, }, { order: 7, - name: 'Jamie Smith', - runs: 347, + name: 'Devon Conway', + runs: 111, }, { order: 8, - name: 'Jamie Overton', - runs: 354, + name: 'Nathan Smith', + runs: 116, }, { order: 9, - name: 'Josh Tongue', - runs: 357, + name: 'Kyle Jamieson', + runs: 124, }, { order: 10, - name: 'Gus Atkinson', - runs: 367, + name: 'Matt Henry', + runs: 138, }, ], - byes: 2, + byes: 1, legByes: 9, - noBalls: 1, + noBalls: 3, penalties: 0, - wides: 11, - extras: 23, + wides: 0, + extras: 13, }, ], - competitionName: 'Fifth Test Match', - venueName: 'The Kia Oval', + competitionName: 'Test Match Series', + stage: 'First Test Match', + venueName: "Lord's", result: 'result', - gameDate: '2025-07-31T10:00:00', + currentDay: 4, + totalDays: 5, + gameDate: '2026-06-04T10:00:00', officials: [ - 'A Raza', - 'H D P K Dharmasena', + 'A T Holdstock', 'R J Tucker', - 'R J Warren', - 'J J Crowe', + 'N N Menon', + 'M Burns', + 'A J Pycroft', ], - matchId: '8c60fe1c-8578-b9b8-e41a-74adcd26277c', + matchId: '6efd7984-0cae-b667-cbaf-754105c422dd', + fullResult: { + resultType: 'home-win', + description: 'England win by 115 runs', + winner: { + winType: 'runs', + margin: '115', + team: 'England', + }, + }, }, nav: { currentUrl: '/cricket', @@ -1112,6 +1108,10 @@ export const cricketMatchData: FECricketMatchPage = { }, ], }, + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'US politics', url: '/us-news/us-politics', @@ -1197,6 +1197,10 @@ export const cricketMatchData: FECricketMatchPage = { url: '/football/results', longTitle: 'football/results', }, + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'Competitions', url: '/football/competitions', @@ -1207,11 +1211,6 @@ export const cricketMatchData: FECricketMatchPage = { url: '/football/teams', longTitle: 'football/teams', }, - { - title: 'Euro 2025', - url: '/football/women-s-euro-2025', - longTitle: 'football/women-s-euro-2025', - }, ], }, { @@ -1330,7 +1329,7 @@ export const cricketMatchData: FECricketMatchPage = { }, { title: 'Columnists', - url: '/index/contributors', + url: '/uk/columnists', }, { title: 'Cartoons', @@ -1352,6 +1351,10 @@ export const cricketMatchData: FECricketMatchPage = { longTitle: 'Sport home', iconName: 'home', children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'Football', url: '/football', @@ -1376,6 +1379,10 @@ export const cricketMatchData: FECricketMatchPage = { url: '/football/results', longTitle: 'football/results', }, + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'Competitions', url: '/football/competitions', @@ -1386,11 +1393,6 @@ export const cricketMatchData: FECricketMatchPage = { url: '/football/teams', longTitle: 'football/teams', }, - { - title: 'Euro 2025', - url: '/football/women-s-euro-2025', - longTitle: 'football/women-s-euro-2025', - }, ], }, { @@ -1639,6 +1641,10 @@ export const cricketMatchData: FECricketMatchPage = { title: 'Sunday quick', url: '/crosswords/series/sunday-quick', }, + { + title: 'Mini', + url: '/crosswords/series/mini-crossword', + }, { title: 'Quick cryptic', url: '/crosswords/series/quick-cryptic', @@ -1777,27 +1783,28 @@ export const cricketMatchData: FECricketMatchPage = { guardianBaseURL: 'https://www.theguardian.com', config: { switches: { - lightbox: true, + prebidCriteo: true, externalVideoEmbeds: true, - personaliseSignInGateAfterCheckout: true, - abSignInGateMainVariant: true, + lightbox: true, + googleOneTapSwitch: true, + hideNewsletterSignupComponentForSubscribers: true, prebidAppnexusUkRow: true, prebidMagnite: true, commercialMetrics: true, - prebidTrustx: true, + prebidTrustx: false, scAdFreeBanner: false, adaptiveSite: true, prebidPermutiveAudience: true, compareVariantDecision: false, - abPrebidAdUnit: false, + manyNewsletterVisibleRecaptcha: false, enableSentryReporting: true, lazyLoadContainers: true, + filterAtAGlance: true, ampArticleSwitch: false, remarketing: true, articleEndSlot: true, keyEventsCarousel: true, registerWithPhone: false, - darkModeWeb: true, targeting: true, remoteHeader: true, ampPrebidOzone: false, @@ -1807,12 +1814,10 @@ export const cricketMatchData: FECricketMatchPage = { prebidAnalytics: true, extendedMostPopular: true, ampContentAbTesting: false, - prebidCriteo: true, - noBoosts: true, imrWorldwide: true, + prebidTeads: true, acast: true, twitterUwt: true, - abAuxiaSignInGate: true, prebidAppnexusInvcode: true, ampPrebidPubmatic: false, a9HeaderBidding: true, @@ -1820,11 +1825,10 @@ export const cricketMatchData: FECricketMatchPage = { enableDiscussionSwitch: true, prebidXaxis: true, stickyVideos: true, - interactiveFullHeaderSwitch: true, discussionAllPageSize: true, + showNewNewsletterSignupCard: true, prebidUserSync: true, audioOnwardJourneySwitch: true, - dcrJavascriptBundle: false, brazeTaylorReport: false, callouts: true, sentinelLogger: true, @@ -1837,27 +1841,29 @@ export const cricketMatchData: FECricketMatchPage = { ampAmazon: false, mostViewedFronts: true, optOutAdvertising: true, - abSignInGateMainControl: true, googleSearch: true, brazeSwitch: true, + signInGate: true, prebidKargo: true, - abAdmiralAdblockRecovery: true, consentManagement: true, + disableChildDirected: true, + productLeftColCards: true, + personaliseSignInGateAfterCheckout: true, idProfileNavigation: true, confiantAdVerification: true, discussionAllowAnonymousRecommendsSwitch: false, - absoluteServerTimes: false, permutive: true, comscore: true, ampPrebidCriteo: false, + prebidLiveramp: true, prebidTheTradeDesk: true, newsletterOnwards: false, youtubeIma: true, + usSignupHideMarketingToggle: true, webFonts: true, liveBlogTopSponsorship: true, lineItemJobs: true, ophan: true, - abGoogleOneTap: false, crosswordSvgThumbnails: true, prebidTriplelift: true, prebidPubmatic: true, @@ -1866,7 +1872,6 @@ export const cricketMatchData: FECricketMatchPage = { enhanceTweets: true, prebidIndexExchange: true, prebidOpenx: true, - loopingVideo: true, prebidHeaderBidding: true, idCookieRefresh: true, discussionPageSize: true, @@ -1878,9 +1883,7 @@ export const cricketMatchData: FECricketMatchPage = { prebidSmart: true, shouldLoadGoogletag: true, inizio: true, - prebidBidCache: true, }, - abTests: {}, serverSideABTests: {}, googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', stage: 'PROD', @@ -1901,9 +1904,9 @@ export const cricketMatchData: FECricketMatchPage = { forecastsapiurl: '/weatherapi/forecast', supportUrl: 'https://support.theguardian.com', commercialBundleUrl: - 'https://assets.guim.co.uk/commercial/d05ba48ca72061c05d80/graun.standalone.commercial.js', + 'https://assets.guim.co.uk/commercial/5fc9045c642159f82da0/graun.standalone.commercial.js', idOAuthUrl: 'https://oauth.theguardian.com', - webTitle: 'Fifth Test Match, The Kia Oval', + webTitle: "First Test Match, Lord's", isFront: false, idWebAppUrl: 'https://oauth.theguardian.com', a9PublisherId: '3722', @@ -1930,7 +1933,7 @@ export const cricketMatchData: FECricketMatchPage = { dfpHost: 'pubads.g.doubleclick.net', sentryPublicApiKey: '344003a8d11c41d8800fbad8383fdc50', pillar: '', - pageId: '/sport/cricket/match/2025-08-04/england-cricket-team', + pageId: '/sport/cricket/match/2026-06-06/england-cricket-team', beaconUrl: '//phar.gu-web.net', discussionD2Uid: 'zHoBy6HNKsk', ophanJsUrl: '//j.ophan.co.uk/ophan.ng', @@ -1984,15 +1987,21 @@ export const cricketMatchData: FECricketMatchPage = { extraClasses: '', }, { - text: 'SecureDrop', - url: 'https://www.theguardian.com/securedrop', - dataLinkName: 'securedrop', + text: 'Contact us', + url: '/help/contact-us', + dataLinkName: 'uk : footer : contact us', + extraClasses: '', + }, + { + text: 'Tip us off', + url: 'https://www.theguardian.com/tips', + dataLinkName: 'uk : footer : tips', extraClasses: '', }, { - text: 'Work for us', - url: 'https://workforus.theguardian.com', - dataLinkName: 'uk : footer : work for us', + text: 'SecureDrop', + url: 'https://www.theguardian.com/securedrop', + dataLinkName: 'securedrop', extraClasses: '', }, { @@ -2008,15 +2017,21 @@ export const cricketMatchData: FECricketMatchPage = { extraClasses: '', }, { - text: 'Terms & conditions', - url: '/help/terms-of-service', - dataLinkName: 'terms', + text: 'Modern Slavery Act', + url: 'https://uploads.guim.co.uk/2025/09/05/Modern_Slavery_Statement_2025.pdf', + dataLinkName: 'uk : footer : modern slavery act statement', extraClasses: '', }, { - text: 'Contact us', - url: '/help/contact-us', - dataLinkName: 'uk : footer : contact us', + text: 'Tax strategy', + url: 'https://uploads.guim.co.uk/2025/09/05/Tax_strategy_for_the_year_ended_31_March_2025.pdf', + dataLinkName: 'uk : footer : tax strategy', + extraClasses: '', + }, + { + text: 'Terms & conditions', + url: '/help/terms-of-service', + dataLinkName: 'terms', extraClasses: '', }, ], @@ -2034,15 +2049,9 @@ export const cricketMatchData: FECricketMatchPage = { extraClasses: '', }, { - text: 'Modern Slavery Act', - url: 'https://uploads.guim.co.uk/2024/09/04/Modern_Slavery_Statement_2024_.pdf', - dataLinkName: 'uk : footer : modern slavery act statement', - extraClasses: '', - }, - { - text: 'Tax strategy', - url: 'https://uploads.guim.co.uk/2024/08/27/TAX_STRATEGY_FOR_THE_YEAR_ENDED_31_MARCH_2025.pdf', - dataLinkName: 'uk : footer : tax strategy', + text: 'Newsletters', + url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', + dataLinkName: 'uk : footer : newsletters', extraClasses: '', }, { @@ -2052,33 +2061,45 @@ export const cricketMatchData: FECricketMatchPage = { extraClasses: '', }, { - text: 'Facebook', - url: 'https://www.facebook.com/theguardian', - dataLinkName: 'uk : footer : facebook', + text: 'Bluesky', + url: 'https://bsky.app/profile/theguardian.com', + dataLinkName: 'uk : footer : Bluesky', extraClasses: '', }, { - text: 'YouTube', - url: 'https://www.youtube.com/user/TheGuardian', - dataLinkName: 'uk : footer : youtube', + text: 'Facebook', + url: 'https://www.facebook.com/theguardian', + dataLinkName: 'uk : footer : Facebook', extraClasses: '', }, { text: 'Instagram', url: 'https://www.instagram.com/guardian', - dataLinkName: 'uk : footer : instagram', + dataLinkName: 'uk : footer : Instagram', extraClasses: '', }, { text: 'LinkedIn', url: 'https://www.linkedin.com/company/theguardian', - dataLinkName: 'uk : footer : linkedin', + dataLinkName: 'uk : footer : LinkedIn', extraClasses: '', }, { - text: 'Newsletters', - url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', - dataLinkName: 'uk : footer : newsletters', + text: 'Threads', + url: 'https://www.threads.com/@guardian', + dataLinkName: 'uk : footer : Threads', + extraClasses: '', + }, + { + text: 'TikTok', + url: 'https://www.tiktok.com/@guardian', + dataLinkName: 'uk : footer : TikTok', + extraClasses: '', + }, + { + text: 'YouTube', + url: 'https://www.youtube.com/user/TheGuardian', + dataLinkName: 'uk : footer : YouTube', extraClasses: '', }, ], @@ -2108,9 +2129,9 @@ export const cricketMatchData: FECricketMatchPage = { extraClasses: '', }, { - text: 'Tips', - url: 'https://www.theguardian.com/tips', - dataLinkName: 'uk : footer : tips', + text: 'Work with us', + url: 'https://workwithus.theguardian.com/', + dataLinkName: 'uk : footer : work with us', extraClasses: '', }, { @@ -2125,6 +2146,6 @@ export const cricketMatchData: FECricketMatchPage = { isAdFreeUser: false, contributionsServiceUrl: 'https://contributions.guardianapis.com', canonicalUrl: - 'https://www.theguardian.com//sport/cricket/match/2025-08-04/england-cricket-team', - pageId: '/sport/cricket/match/2025-08-04/england-cricket-team', + 'https://www.theguardian.com//sport/cricket/match/2026-06-06/england-cricket-team', + pageId: '/sport/cricket/match/2026-06-06/england-cricket-team', }; diff --git a/dotcom-rendering/fixtures/generated/fe-articles/AffiliateProductShowcase.ts b/dotcom-rendering/fixtures/generated/fe-articles/AffiliateProductShowcase.ts new file mode 100644 index 00000000000..041a9eede5f --- /dev/null +++ b/dotcom-rendering/fixtures/generated/fe-articles/AffiliateProductShowcase.ts @@ -0,0 +1,5312 @@ +/** + * DO NOT EDIT THIS FILE! + * + * This file was automatically generated using the gen-fixtures.js script. Any edits + * you make here will be lost. + * + * If the data in these fixtures is not what you expect then + * + * 1. Refresh the data using 'make gen-fixtures' or + * 2. if the latest live data is not what you need, then consider editing + * gen-fixtures.js directly. + */ + +import type { FEArticle } from '../../../src/frontend/feArticle'; + +export const AffiliateProductShowcase: FEArticle = { + version: 3, + headline: + 'From skin-brightening serum to a bargain coffee machine: 10 things you loved most in April', + standfirst: + '

Whether it’s a new season scent or a springy running shoe, your April favourites show you’re ready for a fresh start

\n

Don’t get the Filter delivered to your inbox? Sign up here

', + webTitle: + 'From skin-brightening serum to a bargain coffee machine: 10 things you loved most in April', + affiliateLinksDisclaimer: 'true', + mainMediaElements: [ + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'showcase', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '5:4', + height: '112', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/140.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '5:4', + height: '400', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/500.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '5:4', + height: '800', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/1000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '5:4', + height: '1600', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/2000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '5:4', + height: '4000', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/5000.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '5:4', + isMaster: 'true', + height: '4000', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg', + }, + ], + }, + elementId: 'af50bfd3-88e6-46e3-a583-c29e09a24602', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=16d1592a38fb379949130402a526211f', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e17ad80f0b8be77a1d9cf45812f208c6', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=700&quality=85&auto=format&fit=max&s=d5bc9f9598a1b6670ac8cb91a6d0eee6', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=ad30e808e359bcf4ea25e438785f60b6', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=16d1592a38fb379949130402a526211f', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e17ad80f0b8be77a1d9cf45812f208c6', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=645&quality=85&auto=format&fit=max&s=e89deea3764c76a5b8659386a99b8b26', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=a17d5ce05c95c00b044b85c016972291', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=465&quality=85&auto=format&fit=max&s=598e5c25f35f66c33562016fccbb1cd8', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=fac1cf7593bb4cd33036d2a73463df7f', + width: 930, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [], + }, + { + weighting: 'supporting', + srcSet: [], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1020&quality=85&auto=format&fit=max&s=9a72d6da2264f309caa001bf5e0799a5', + width: 1020, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1020&quality=45&auto=format&fit=max&dpr=2&s=02461170590cfd04d507f2d282fdc3f2', + width: 2040, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=940&quality=85&auto=format&fit=max&s=e0f0262fe6d5c5dcea88bd85937a1beb', + width: 940, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=940&quality=45&auto=format&fit=max&dpr=2&s=78eb4106ee5ac7f63efe4b42438d8268', + width: 1880, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=700&quality=85&auto=format&fit=max&s=d5bc9f9598a1b6670ac8cb91a6d0eee6', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=ad30e808e359bcf4ea25e438785f60b6', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=700&quality=85&auto=format&fit=max&s=d5bc9f9598a1b6670ac8cb91a6d0eee6', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=ad30e808e359bcf4ea25e438785f60b6', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=660&quality=85&auto=format&fit=max&s=d1fb4eaf11b89ae2b76e49c31cabc17e', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=ae524303bf1a2fb4e89bc96aec4aad36', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=645&quality=85&auto=format&fit=max&s=e89deea3764c76a5b8659386a99b8b26', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=a17d5ce05c95c00b044b85c016972291', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=465&quality=85&auto=format&fit=max&s=598e5c25f35f66c33562016fccbb1cd8', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=fac1cf7593bb4cd33036d2a73463df7f', + width: 930, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1900&quality=85&auto=format&fit=max&s=716d37de2f1bd3eb13cdaea43c8d18da', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=256d986779295aea9b8efc70bb063ce3', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1300&quality=85&auto=format&fit=max&s=792e34b42c474949957211d4e3d56ae4', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=8f061d1f1ce72a80b99450e7a54fca0b', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1140&quality=85&auto=format&fit=max&s=7b54b007ebcb1d141a3f1fb824bd612e', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=6a38c6cffb31117f6981047b526be1d7', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=980&quality=85&auto=format&fit=max&s=5916e97d6bafeeeccf7728bab94f5bfa', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=5c5fe0449e0296b16c5f86383d3f4cac', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=740&quality=85&auto=format&fit=max&s=2ee4cdd22f43014c3eea3ea12d08f5f3', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=3b54a0ad6314a15601efc7b586732b30', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=660&quality=85&auto=format&fit=max&s=d1fb4eaf11b89ae2b76e49c31cabc17e', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=ae524303bf1a2fb4e89bc96aec4aad36', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=480&quality=85&auto=format&fit=max&s=242514eea1c4ef4a5209f520f909f52a', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/1a10a44dd770d568e68ad39ba99292cec02dc8da/0_0_5000_4000/master/5000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=3d66359e9294590b2ad97d61ae5fe205', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Flat lay of running shoe, suitcase, and vitamin serum for May newsletter', + credit: 'Composite: PR Image', + }, + }, + ], + main: '
Flat lay of running shoe, suitcase, and vitamin serum for May newsletter
', + keyEvents: [], + blocks: [ + { + id: '667d2c5e8f08db81fc3259c7', + elements: [ + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

It’s easy to feel hopeful in spring, with blossom all around and sunny days bringing the promise of summer ahead. It feels like a fresh start, and it’s clear from your favourite things in April that you’re looking for rejuvenation.

', + elementId: 'a7aa303b-6665-48a9-9725-18a148f7e745', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Maybe that’s a new scent, or a cabin bag for a holiday. Perhaps it’s a health reset, with a pair of running shoes to kickstart better habits, or a celebrity-endorsed supplement. You’ve also loved sub-£20 skincare basics and high-street looks inspired by Matthieu Blazy’s Chanel. Here are your favourite things from April.

', + elementId: 'c2ba3036-e163-4d95-84ea-6aa5876478e4', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '1778d18d-397c-4591-8627-0e37979728c1', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The bean-to-cup coffee machine

', + elementId: '330ef306-8e43-428f-877a-65cb7807f963', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/4000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/140.jpg', + }, + ], + }, + elementId: 'fa37f13d-b4e7-4245-a2bc-7044b7ecf576', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=6385c9bcbcd4c33aa8fe1f06a3cf7346', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=c4e9a66470d2513cfaf26c177bca2c9d', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=69929331489ee5c83d40fd786c68e9d8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a8fd8dd445232e7443cc9261251db6d3', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=5f83cb697d146aa901ea0c1b2fcbd7a7', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0a01d93388d7d3dc136f87b445cee85c', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=140&quality=85&auto=format&fit=max&s=13bb60e6f09202b6916373649607ab59', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=47bbffdcfe425055e728ec1ec20fafbd', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=120&quality=85&auto=format&fit=max&s=95b18e22fbda661aaab1edac7798c800', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=541a1a8d53a65da56716929a555af9bf', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=380&quality=85&auto=format&fit=max&s=83092386b4441085a0a454481f5c8c8b', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=fbd95793dd8a14d7fc4f728e2fd9ddfb', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=300&quality=85&auto=format&fit=max&s=ca20d4c65c483b1d8054efe91b43ba46', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=1011a758f21f27b2dd53b7e2c6354ac2', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=6385c9bcbcd4c33aa8fe1f06a3cf7346', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=c4e9a66470d2513cfaf26c177bca2c9d', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=69929331489ee5c83d40fd786c68e9d8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a8fd8dd445232e7443cc9261251db6d3', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=5f83cb697d146aa901ea0c1b2fcbd7a7', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0a01d93388d7d3dc136f87b445cee85c', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=860&quality=85&auto=format&fit=max&s=fe9c55099f92b011f3857191df8a6c2c', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=196bb96b7bea59fe91953405dceb56c0', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=780&quality=85&auto=format&fit=max&s=a2c3f5cb83e7187a61d5f23d43044b2d', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=15c709c4d2dca42568a9b4f40a61ef92', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=6385c9bcbcd4c33aa8fe1f06a3cf7346', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=c4e9a66470d2513cfaf26c177bca2c9d', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=69929331489ee5c83d40fd786c68e9d8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a8fd8dd445232e7443cc9261251db6d3', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=5f83cb697d146aa901ea0c1b2fcbd7a7', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0a01d93388d7d3dc136f87b445cee85c', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=6385c9bcbcd4c33aa8fe1f06a3cf7346', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=c4e9a66470d2513cfaf26c177bca2c9d', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=69929331489ee5c83d40fd786c68e9d8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a8fd8dd445232e7443cc9261251db6d3', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=5f83cb697d146aa901ea0c1b2fcbd7a7', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0a01d93388d7d3dc136f87b445cee85c', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=1900&quality=85&auto=format&fit=max&s=f3f9b3be1b93fa73a48c4bfcb4fe9c40', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=c9d4307c97cc2cfda3f73c041822c283', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=1300&quality=85&auto=format&fit=max&s=d77b1fc1a21e2858c7280a9e5f6de561', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=3eea1d7e174e30f607f751f9bc25d124', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=1140&quality=85&auto=format&fit=max&s=cc32078ad13e111b771fd88c762e4dc4', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=ac9ee630fca00b2f233a6b1d3e7b2573', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=980&quality=85&auto=format&fit=max&s=846545f58542c923b088910e10136de7', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=5d82cee2f4c7677533872be9d044b37f', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=740&quality=85&auto=format&fit=max&s=46eed800389808c86da9bd90230e4dac', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=cd277dcbac22d035a595820b0ca4ebaf', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=660&quality=85&auto=format&fit=max&s=240063c2440b6d532221ba0b1fe3983e', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=b1589b0cae2a2926a92ae2b6bbe1fd67', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=480&quality=85&auto=format&fit=max&s=b864fc6c6ba14867b2cbce24c37d2b30', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/ca8f61c09c1dfa962835c914ab21c5636f9d084a/500_0_4000_4000/master/4000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=e30a7e4a108dfe08606372125150c37a', + width: 960, + }, + ], + }, + ], + data: { + alt: 'De’Longhi Magnifica Start ECAM220.61 Automatic Bean to Cup Coffee Machine, Black', + credit: 'Photograph: De’Longhi', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

De’Longhi Magnifica Start

', + elementId: 'c25209c1-9ac7-4414-8362-0617781823fd', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.johnlewis.com%2Fdelonghi-magnifica-start-ecam220-61-automatic-bean-to-cup-coffee-machine-black%2Fp114206561&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£299 at John Lewis', + elementId: 'ab2037c1-6b72-4a9e-ac1d-eace2c215430', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Our coffee expert, Sasha Muller, once again tested the limits of his caffeine consumption by trialling 12 of the best bean-to-cup machines. The De’Longhi Magnifica Start was his favourite, and was also, by far, your favourite item this month – perhaps you agree that “if the best things in life are free, then the second-best things are cheap”.

', + elementId: '78f3ec68-6b86-426f-ad5a-a7f93f5d192f', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '00b74757-7248-4bbf-9e75-de7fc0fed322', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

A designer scent without the price tag

', + elementId: '14cc40d7-2eb5-4065-8288-5e2ef1e30026', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Essential Parfums discovery set, 10 x 2ml

', + elementId: '445f7de2-c05b-4b38-9fb8-9190ba773d2e', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + height: '4000', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/5000.jpg', + }, + { + index: 1, + fields: { + isMaster: 'true', + height: '4000', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg', + }, + { + index: 2, + fields: { + height: '1600', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/2000.jpg', + }, + { + index: 3, + fields: { + height: '800', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/1000.jpg', + }, + { + index: 4, + fields: { + height: '400', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/500.jpg', + }, + { + index: 5, + fields: { + height: '112', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/140.jpg', + }, + ], + }, + elementId: 'c9fe4405-f017-497b-ac22-685f07e0e06f', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=9a56a449002e9463083ed8abd99276e0', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=891c607833641bfb060ef8a4f766abc7', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=1f2d7187541fb902721ab83e6eea910f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=83657d7b0a51d525115bb7f7b7056a57', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=c8d34c9a407303843e8d7bb3a2647d19', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=1e8eb5d4837c33f7a30c523dba17c8ea', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=140&quality=85&auto=format&fit=max&s=d98aa073235cc15901765d4633947386', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=77bcdc8d29e2bce6277a7f1af383ce44', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=120&quality=85&auto=format&fit=max&s=81ae35317f79dcbce64a4e475d7a7f8a', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=239d4722e2c16a85a0ee2e35d087a5db', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=380&quality=85&auto=format&fit=max&s=ea8b120ac27ab8643d69845dd54a9e0b', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=efcca7cfa42ba475f8080c728f343c14', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=300&quality=85&auto=format&fit=max&s=2e410401d6714920a14b1afc6ffc925a', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=a6bda264118175b776c8e7418a6a0f44', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=9a56a449002e9463083ed8abd99276e0', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=891c607833641bfb060ef8a4f766abc7', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=1f2d7187541fb902721ab83e6eea910f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=83657d7b0a51d525115bb7f7b7056a57', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=c8d34c9a407303843e8d7bb3a2647d19', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=1e8eb5d4837c33f7a30c523dba17c8ea', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=860&quality=85&auto=format&fit=max&s=6874c95e7414db57960be62123c986cf', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=b0b4ba7a7ac386e4186d71d5677542d5', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=780&quality=85&auto=format&fit=max&s=958e408273ab1793ba005d30c662b540', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=804005765cccd33352ff9338b67cc91e', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=9a56a449002e9463083ed8abd99276e0', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=891c607833641bfb060ef8a4f766abc7', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=1f2d7187541fb902721ab83e6eea910f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=83657d7b0a51d525115bb7f7b7056a57', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=c8d34c9a407303843e8d7bb3a2647d19', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=1e8eb5d4837c33f7a30c523dba17c8ea', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=9a56a449002e9463083ed8abd99276e0', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=891c607833641bfb060ef8a4f766abc7', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=1f2d7187541fb902721ab83e6eea910f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=83657d7b0a51d525115bb7f7b7056a57', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=c8d34c9a407303843e8d7bb3a2647d19', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=1e8eb5d4837c33f7a30c523dba17c8ea', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=1900&quality=85&auto=format&fit=max&s=acf2d69ed2a867b7b49e3da8a2c11b74', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=18e7281bff4ea48523938e6e316715bf', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=1300&quality=85&auto=format&fit=max&s=99a394cf3521a92785f672ac11ae665a', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=7f8f939b62ca24b243414a7094e184aa', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=1140&quality=85&auto=format&fit=max&s=bf2db1d76640b6ab45fa23a5746adab5', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=7899753a33cb909a9dcd2a6fd85ea534', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=980&quality=85&auto=format&fit=max&s=4900fd2350054eb5f0b0976bb85ff32e', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=4549b86573902ecbf4b9177f48fa7c65', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=740&quality=85&auto=format&fit=max&s=b99dc4d07823bbebd7364a2250c762f1', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=c6eae290683522a1cc70419e56c59251', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=660&quality=85&auto=format&fit=max&s=3b535d498cf552169026f723e63cbd1c', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=1aa223b9319da388e4fb0d0e2818d2b4', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=480&quality=85&auto=format&fit=max&s=06270bb38c9b1c94c597c7bc6efc4225', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/b5abbbe1f53ac7a9d48617ac26ff4065ab7d43e5/0_0_5000_4000/master/5000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=4cf73b5341255a7a12053a815bfeed3c', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Essential Parfums Discovery Set', + credit: 'Photograph: PR Image', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2F50-ml.co.uk%2Fessential-parfums-discovery-set&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£24.08 at 50ml UK', + elementId: '21dc15b9-5561-4018-a8bf-a5c6dfa87d7a', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

In a recent column, Sali Hughes was excited to see John Lewis selling Essential Parfums, a perfume brand that is “doing things honestly, authentically and with great care”. She recommended an 11-piece discovery set of 2ml samples if “100ml seems like a gamble”, which is now sold out at John Lewis, but this set of 10 is still available. They’re perfect for weekends away, too.

', + elementId: 'f4007e8b-0b87-407a-8fbe-2716e03e3021', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '56c6ee25-9464-4de2-bea3-929c647d4e00', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The best budget cabin bag

', + elementId: 'abc866c7-7563-45d2-994e-5b43e581d2ee', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/4000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/140.jpg', + }, + ], + }, + elementId: '878e06d9-eae0-4f6e-a6fd-c79fa7886603', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=01d27cb30dec87f239ff34faddfa968f', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=58b26f41687d829d2d9cda357dbaff28', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=7c3f2d2523fdb3dd88de0a0fc4c943e3', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=2a78ebd8283b79f3e930b1b1f419ae8e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=73c046d78f34e5270f89aa3d2d833b7f', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=231d749bcf21eeea166f8eb157ec3d70', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=140&quality=85&auto=format&fit=max&s=3c7173819ef9309f782c7ba514c79ec2', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=3180c6ca9e255be18393940973d029eb', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=120&quality=85&auto=format&fit=max&s=9e2a98700bce7d85de1bd86bdcdf3c29', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=bd6f1601ced01ecaa5b619ef4652be0b', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=380&quality=85&auto=format&fit=max&s=0a30af99a2f6e3e7213588906db601a5', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=661e88ce5fcbcb5de6d0dca3ea3bc7e5', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=300&quality=85&auto=format&fit=max&s=aa0dd99be0731e83185d11afc665a91d', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=dc21cfd845bd296a2dd7a62789d79b6b', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=01d27cb30dec87f239ff34faddfa968f', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=58b26f41687d829d2d9cda357dbaff28', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=7c3f2d2523fdb3dd88de0a0fc4c943e3', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=2a78ebd8283b79f3e930b1b1f419ae8e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=73c046d78f34e5270f89aa3d2d833b7f', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=231d749bcf21eeea166f8eb157ec3d70', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=860&quality=85&auto=format&fit=max&s=84c4d3effa2015873d540e38215ed3bb', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=6e5f87d8e7d551dbd7b9db7862376a75', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=780&quality=85&auto=format&fit=max&s=133e27c06c1f6eb2428a52e0fe563311', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=6fb1de2d8f5465f91820bb30feae6849', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=01d27cb30dec87f239ff34faddfa968f', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=58b26f41687d829d2d9cda357dbaff28', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=7c3f2d2523fdb3dd88de0a0fc4c943e3', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=2a78ebd8283b79f3e930b1b1f419ae8e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=73c046d78f34e5270f89aa3d2d833b7f', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=231d749bcf21eeea166f8eb157ec3d70', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=01d27cb30dec87f239ff34faddfa968f', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=58b26f41687d829d2d9cda357dbaff28', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=7c3f2d2523fdb3dd88de0a0fc4c943e3', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=2a78ebd8283b79f3e930b1b1f419ae8e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=73c046d78f34e5270f89aa3d2d833b7f', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=231d749bcf21eeea166f8eb157ec3d70', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=1900&quality=85&auto=format&fit=max&s=a161a8c96fe806a88fac0fb1c0be6d27', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=3bdaf02037f47f90cfc14408a0dfab5b', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=1300&quality=85&auto=format&fit=max&s=d259fbafb1e642c3dce078208e6b2e1e', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=261ae678f0c5a114a5f59c805026dc01', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=1140&quality=85&auto=format&fit=max&s=cefadcf164877ec57cdedfc2f638ae3f', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=ad1f2d97e872fd2c6ae24b3960e699f7', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=980&quality=85&auto=format&fit=max&s=cd31483bd8688fd74e5ba030039c8ce6', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=533a66379635c4b3239a7d03b172ac0a', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=740&quality=85&auto=format&fit=max&s=65c1c3f737ec159f86b949ad7a537181', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=30cd170d2cfca61ec6eee93f433988f7', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=660&quality=85&auto=format&fit=max&s=c5da3d0f91b365e4a0bbac41ef6f7354', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=7bcc58601b7080f6f2d98e176a7a9abb', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=480&quality=85&auto=format&fit=max&s=c7c8b53de8ddddf44996436d6f7ec7c5', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/6ec0f8918f5c97bdaf15da922dc4d49995d06c32/500_0_4000_4000/master/4000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=ad04b35e28d1c6d0b06f4c03fdaeae4c', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Tripp Holiday 8 Turquoise Large Suitcase', + credit: 'Photograph: Tripp', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Tripp Holiday 8 suitcase

', + elementId: '1716c709-257e-4237-a587-66d7f0c49601', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.amazon.co.uk%2FTRIPP-Holiday-Turquoise-Suitcase-55x40x20cm%2Fdp%2FB0GGSJB9X1%2F&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£49.50 at Amazon', + elementId: '34f8a134-dbc9-4965-8fc7-55f1b587417f', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Pete Wise proved his dedication to thorough testing by manoeuvring the best carry-on luggage around a muddy assault course in Leeds – all while dressed in a holiday-appropriate Hawaiian shirt and shorts. Our bargain-loving readers loved the Tripp Holiday 8, his top budget pick, which “proved a nimble option over the assault course: lightweight, balanced and capable of surviving a tumultuous journey”.

', + elementId: 'a517cfaa-1183-43d4-9b02-4d006aa7733a', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '40163c99-6866-4822-beb6-b4debc27caae', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The Chanel-style straight-leg jeans

', + elementId: 'ded071ae-709e-4c2b-bcb0-8d5ea4150b90', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/4000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/140.jpg', + }, + ], + }, + elementId: '42baafe9-b0f3-4375-8c76-8fb9eabb1538', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=93301c7385f1dec22e282ee13536e2bf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e7a663e9b32cf1f22735386b6fc53f91', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=af54311109feea738fb039484a7ca327', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=e975457e5999be1b19d681fce25fedca', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e0309f9821173fd204f3c31b463eb79d', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=d7bd61404365173fed6e6c528bcdff95', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=140&quality=85&auto=format&fit=max&s=9a625ea961c61d459f7588a5edbeaa5d', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=01566adb8a6deec15657dfda132d1cf7', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=120&quality=85&auto=format&fit=max&s=84d71fb006668a40328c5f868300fb56', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=d436e7d3e2ce25e342d3c1970d9f2a11', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=380&quality=85&auto=format&fit=max&s=28ec700692334697a933b4ef0af9220a', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=2db69abd199374b5f7cf9217a6583ab7', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=300&quality=85&auto=format&fit=max&s=8ab0d4b0cc58dc72cb5d9864c6f03448', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=3afd4d19bc81cfe30858abfaea321e9a', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=93301c7385f1dec22e282ee13536e2bf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e7a663e9b32cf1f22735386b6fc53f91', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=af54311109feea738fb039484a7ca327', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=e975457e5999be1b19d681fce25fedca', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e0309f9821173fd204f3c31b463eb79d', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=d7bd61404365173fed6e6c528bcdff95', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=860&quality=85&auto=format&fit=max&s=d1e100803027508fda26da1751bf861b', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=506530e3382fb80a8a55520a762e3c53', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=780&quality=85&auto=format&fit=max&s=ae5b454c76c6b1fd58ede4fbfe39b8fb', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=c57957758f0a20073168e32a6e24a35d', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=93301c7385f1dec22e282ee13536e2bf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e7a663e9b32cf1f22735386b6fc53f91', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=af54311109feea738fb039484a7ca327', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=e975457e5999be1b19d681fce25fedca', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e0309f9821173fd204f3c31b463eb79d', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=d7bd61404365173fed6e6c528bcdff95', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=93301c7385f1dec22e282ee13536e2bf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=e7a663e9b32cf1f22735386b6fc53f91', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=af54311109feea738fb039484a7ca327', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=e975457e5999be1b19d681fce25fedca', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e0309f9821173fd204f3c31b463eb79d', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=d7bd61404365173fed6e6c528bcdff95', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=1900&quality=85&auto=format&fit=max&s=07c0f313941211b4039babad781c20bc', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=de8a049ff6bd55f509c3fad8d0e4798a', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=1300&quality=85&auto=format&fit=max&s=f2bac84e0a9f1e87a411f1ac5376bc56', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=c645f3496f837b7ea1a460e463f1b649', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=1140&quality=85&auto=format&fit=max&s=aab31120404297e252dc0661bde8477c', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=d614c9ea22525b2cb5b52035d76b929d', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=980&quality=85&auto=format&fit=max&s=66bb55a6c2e39b1ac57670cc96c2302b', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=f041fc766b0e28d1457e60f08063aa25', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=740&quality=85&auto=format&fit=max&s=de85d821658daf86ea17d4538e286e46', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=10a0f012303762c774e75896db57075a', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=660&quality=85&auto=format&fit=max&s=e79a9c3aacf597c101dde4317a79f976', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=87bd1a34c326d7fc77979fd3b2be0331', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=480&quality=85&auto=format&fit=max&s=6c4e7f01f00a96b93660d6462e92f3c5', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/20caeb230ef4d434b307ce78c41d96466f1c9247/500_0_4000_4000/master/4000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=144ecc8a6ede99315e5520313242605d', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Uniqlo Baggy Jeans', + credit: 'Photograph: Uniqlo', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Women’s baggy jeans

', + elementId: '78955c6c-8090-4f4e-8ecc-db20d1a8c030', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.uniqlo.com%2Fuk%2Fen%2Fproducts%2FE483999-000%2F00%3FcolorDisplayCode%3D65%26sizeDisplayCode%3D024%26pldDisplayCode%3D033&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£34.90 at Uniqlo', + elementId: 'e10b0a6c-b5ed-4965-b02b-09dec65e59ef', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Matthieu Blazy’s first video campaign for Chanel features Margot Robbie in a bouclé jacket and straight-leg jeans – an “outfit formula that is proving to be consumer catnip”, wrote Chloe Mac Donnell in a recent piece. These JW Anderson for Uniqlo jeans “are a close match” (as is Mango’s tweed jacket), and proved to be catnip for our readers too.

', + elementId: 'd13ee651-7951-4257-ae28-2576a7739e62', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '0dba66cd-10d7-4a6b-93bd-9169ee5fc9f2', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The bargain vitamin C serum

', + elementId: '1685a1b1-6aff-45cf-8286-9cc51c8faed6', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/2000.png', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/2000.png', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/1000.png', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/500.png', + }, + { + index: 5, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/png', + url: 'https://media.guim.co.uk/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/140.png', + }, + ], + }, + elementId: 'da180c0e-eecd-471d-8db0-176355a35707', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=85&auto=format&fit=max&s=9ecdb307638b12d7892b0ce2f1432244', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=4be12b2755de45bc813b1f0dc0785354', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=85&auto=format&fit=max&s=8d66584a59adcffc8fce56ded75c725b', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=1449c6c288b6fa0a8a8982ee313a35ac', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=85&auto=format&fit=max&s=80b5df5bd232e865efb84f58ac340e7b', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=e1f5fcb6d18269ec6bb554a5f5048458', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=140&quality=85&auto=format&fit=max&s=6780900fa947da18f3611b96287ff5cf', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=140&quality=45&auto=format&fit=max&dpr=2&s=9e85222d921f825c92152aaceceb4174', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=120&quality=85&auto=format&fit=max&s=a72117b1791e821593f60f2f47bc1195', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=120&quality=45&auto=format&fit=max&dpr=2&s=03e179f238c6ea60eadc2185b498497b', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=380&quality=85&auto=format&fit=max&s=8ea7eaf15910e4940fec616619a37517', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=380&quality=45&auto=format&fit=max&dpr=2&s=e11ae2ab98bb6e9758d4419c7761a50f', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=300&quality=85&auto=format&fit=max&s=cac964e74b6babf37f1228323c155439', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=300&quality=45&auto=format&fit=max&dpr=2&s=bae29d284a5eab6158ccd75a02411404', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=85&auto=format&fit=max&s=9ecdb307638b12d7892b0ce2f1432244', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=4be12b2755de45bc813b1f0dc0785354', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=85&auto=format&fit=max&s=8d66584a59adcffc8fce56ded75c725b', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=1449c6c288b6fa0a8a8982ee313a35ac', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=85&auto=format&fit=max&s=80b5df5bd232e865efb84f58ac340e7b', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=e1f5fcb6d18269ec6bb554a5f5048458', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=860&quality=85&auto=format&fit=max&s=db8a36e49e6d9453b24f400c40a8ed0d', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=860&quality=45&auto=format&fit=max&dpr=2&s=a993d6947c2005652b992bdd16ce1072', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=780&quality=85&auto=format&fit=max&s=c5c84f807db8d728fd477cc3402750e1', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=780&quality=45&auto=format&fit=max&dpr=2&s=46acb313cd3a571c0907483a5e429a81', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=85&auto=format&fit=max&s=9ecdb307638b12d7892b0ce2f1432244', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=4be12b2755de45bc813b1f0dc0785354', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=85&auto=format&fit=max&s=8d66584a59adcffc8fce56ded75c725b', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=1449c6c288b6fa0a8a8982ee313a35ac', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=85&auto=format&fit=max&s=80b5df5bd232e865efb84f58ac340e7b', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=e1f5fcb6d18269ec6bb554a5f5048458', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=85&auto=format&fit=max&s=9ecdb307638b12d7892b0ce2f1432244', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=620&quality=45&auto=format&fit=max&dpr=2&s=4be12b2755de45bc813b1f0dc0785354', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=85&auto=format&fit=max&s=8d66584a59adcffc8fce56ded75c725b', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=605&quality=45&auto=format&fit=max&dpr=2&s=1449c6c288b6fa0a8a8982ee313a35ac', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=85&auto=format&fit=max&s=80b5df5bd232e865efb84f58ac340e7b', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=445&quality=45&auto=format&fit=max&dpr=2&s=e1f5fcb6d18269ec6bb554a5f5048458', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=1900&quality=85&auto=format&fit=max&s=9b6f80f25fd1c2442304e2ffcd16937f', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=1900&quality=45&auto=format&fit=max&dpr=2&s=6a325cbb109797f407c9824f658dc8eb', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=1300&quality=85&auto=format&fit=max&s=3d8dc3460a8ee98e7fada9334e45d61a', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=1300&quality=45&auto=format&fit=max&dpr=2&s=22804a0cd51703de1c6ba7f44a30b87e', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=1140&quality=85&auto=format&fit=max&s=09154c734317436134c2df49e6727a41', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=1140&quality=45&auto=format&fit=max&dpr=2&s=4236e488114862aefee839d97e412723', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=980&quality=85&auto=format&fit=max&s=97056c902ea0b39520affcd3f8e747ac', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=980&quality=45&auto=format&fit=max&dpr=2&s=57b2b6f91530c5fcfc55196f02077cce', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=740&quality=85&auto=format&fit=max&s=fc126758d571836199b20c88daf44ea5', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=740&quality=45&auto=format&fit=max&dpr=2&s=6250124ec8bb098929c3394f73e586b4', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=660&quality=85&auto=format&fit=max&s=a20bfb35b5080bfa9f3aa202e4625c41', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=660&quality=45&auto=format&fit=max&dpr=2&s=7ab74ea3a86491f6b262e2e25f989049', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=480&quality=85&auto=format&fit=max&s=85ad97faeb4c587040e499c1647002f3', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/e2fd8c2e6c61038f5d72878736c1b7dfe6ff4405/0_0_2000_2000/master/2000.png?width=480&quality=45&auto=format&fit=max&dpr=2&s=1e4265ee2009fb91db8192a02e5bab26', + width: 960, + }, + ], + }, + ], + data: { + alt: 'e.l.f. SKIN Vitamin C + E + ferulic Serum', + credit: 'Photograph: Elf', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Elf Skin Brighten + Glow vitamin C + E + ferulic serum, 30ml

', + elementId: 'aaeecce8-ce3b-406d-87ae-57ddf4e92ad7', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.amazon.co.uk%2Fgp%2Fproduct%2FB0F3JCDFVT&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£14.45 at Amazon', + elementId: '6c997ea6-8859-4451-af55-55537da7c377', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

On hearing that this sub-£20 vitamin C serum contains the “exact same percentages of vitamin C, E and ferulic acid as SkinCeuticals’ gold standard vitamin C product”, I rushed to buy it – as did many of you. It was Danielle Wilkins’ top budget pick after four months of testing 15 of the best vitamin C serums, finding it “extremely hydrating” and leaving her skin “feeling softer with continued use”.

', + elementId: '1eacbc20-7dcb-46eb-acef-ecdca25dab40', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '145b2640-2455-46b9-b2e4-fe566aef3f4f', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The idiot-proof recipe box

', + elementId: '63b567bd-ebd7-4d0a-9ca1-f6335d495efc', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '5:4', + height: '1677', + width: '2096', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/2096.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '5:4', + isMaster: 'true', + height: '1677', + width: '2096', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '5:4', + height: '1600', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '5:4', + height: '800', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '5:4', + height: '400', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '5:4', + height: '112', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/140.jpg', + }, + ], + }, + elementId: '86fdef5a-920e-492a-aea2-7385c89ffdfa', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=85&auto=format&fit=max&s=d88ac7108159e77bee1b5f70bb137653', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f9a005e77ae6e8e640949ff1c9f3e44e', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=85&auto=format&fit=max&s=d33cbd59516530347bd85e85a840fe2f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a4b7a6188a45bc71c05ab1dbe749ddc7', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=85&auto=format&fit=max&s=efcb989f30530a4bdea1c6a8dd553d08', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=30b0f7e3527f806ed33bfff1c417bb27', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=140&quality=85&auto=format&fit=max&s=7ab11ff58b7b422ef62cbb8aa14b2d5b', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=7364e87a94a1f6546ccf9460d4845a64', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=120&quality=85&auto=format&fit=max&s=99fe23e55d6e04cd6dc0c47d5f163bfd', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=a976dae209dbaff2197fdf7ddc422e95', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=380&quality=85&auto=format&fit=max&s=8f92384ec88fda0f7ecc62505c20bff4', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=193c67ff758c8550afbad8b9ab031f15', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=300&quality=85&auto=format&fit=max&s=3685d0cd96b7d1da3f842d3bdd032558', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=589c399567f3b70bd3a98169024bec69', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=85&auto=format&fit=max&s=d88ac7108159e77bee1b5f70bb137653', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f9a005e77ae6e8e640949ff1c9f3e44e', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=85&auto=format&fit=max&s=d33cbd59516530347bd85e85a840fe2f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a4b7a6188a45bc71c05ab1dbe749ddc7', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=85&auto=format&fit=max&s=efcb989f30530a4bdea1c6a8dd553d08', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=30b0f7e3527f806ed33bfff1c417bb27', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=860&quality=85&auto=format&fit=max&s=f528e9775712f152274589dc3325fc1f', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=f6ee0ea9295cea46e46ea0c9dab306a8', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=780&quality=85&auto=format&fit=max&s=a6c533be90f7745199787a26999dfcb7', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=cb6a77f6d1940d20036e878e949ee326', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=85&auto=format&fit=max&s=d88ac7108159e77bee1b5f70bb137653', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f9a005e77ae6e8e640949ff1c9f3e44e', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=85&auto=format&fit=max&s=d33cbd59516530347bd85e85a840fe2f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a4b7a6188a45bc71c05ab1dbe749ddc7', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=85&auto=format&fit=max&s=efcb989f30530a4bdea1c6a8dd553d08', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=30b0f7e3527f806ed33bfff1c417bb27', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=85&auto=format&fit=max&s=d88ac7108159e77bee1b5f70bb137653', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f9a005e77ae6e8e640949ff1c9f3e44e', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=85&auto=format&fit=max&s=d33cbd59516530347bd85e85a840fe2f', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=a4b7a6188a45bc71c05ab1dbe749ddc7', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=85&auto=format&fit=max&s=efcb989f30530a4bdea1c6a8dd553d08', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=30b0f7e3527f806ed33bfff1c417bb27', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=1900&quality=85&auto=format&fit=max&s=3b8ffdf11d1c123931ad9e9804ae3cf4', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=e0395bedd23d1f95e6f49d3e46f05049', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=1300&quality=85&auto=format&fit=max&s=678020557dbf23ac554b90cfca238d6a', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=fcd48b6766623f952d35096e8c1d5da2', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=1140&quality=85&auto=format&fit=max&s=a9593eb47858a66b219e110c206c6716', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=6e5b601eef600b21f58c7b5c98c1461e', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=980&quality=85&auto=format&fit=max&s=9f758fe007ef3b0b38c471f2cf030be1', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=0a33fe45a5fea823ee32ff99c80f222b', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=740&quality=85&auto=format&fit=max&s=6e68a5cbfa7667b0aae9b3a46be7f80f', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=2d32a6cd9f4e8095b064374d9353f961', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=660&quality=85&auto=format&fit=max&s=b780bcf007207c6b12a671f36d1e2631', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=163804a1767aafe9b33b1b1215fe70ce', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=480&quality=85&auto=format&fit=max&s=75ef7143125b317de90641cb477eac3a', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/52f0b63abc10e46f130f9ec6b28bd22e59824a6a/449_0_2096_1677/master/2096.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=2d13e867ee0e1007c1bda50924adf118', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Simply Cook Spice subscription service', + credit: 'Photograph: SimplyCook', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

SimplyCook, two-person meal

', + elementId: 'c82737dd-24d4-4061-b7ba-3a5fdb0bd122', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.simplycook.com%2F&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: 'From £2.50 at SimplyCook', + elementId: '3b796d3a-20fe-442f-8b71-e086a6f457d9', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

As with mattresses, Jane Hoskyn’s family played an important role in testing the best meal delivery kits, with a range of dietary requirements and cooking skills providing a well-rounded test panel. SimplyCook was by far the cheapest, as it doesn’t include any fresh ingredients and fits through the letterbox. “SimplyCook’s chef-prepared pots are what my dad, Don, calls ‘the interesting bit of cooking’,” wrote Jane, with “idiot-proof” recipes that are “easy to customise”. This simple approach was the one that appealed to you the most.

', + elementId: '745c92c3-60cf-45eb-bd43-528539146aa7', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: 'f97d521a-8fea-4fd2-9e4a-6f2703c37802', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The bargain leather bag

', + elementId: '3542d78b-defb-4e00-b0ff-2d710d679738', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '5:4', + height: '4000', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/5000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '5:4', + isMaster: 'true', + height: '4000', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '5:4', + height: '1600', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '5:4', + height: '800', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '5:4', + height: '400', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '5:4', + height: '112', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/140.jpg', + }, + ], + }, + elementId: '1e05c1c8-cb7f-45c6-a83a-f9e148bbee27', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=c43833d5667a6d16acca99b7c4873c96', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f44f8849f904da4a348b9d11d29b0928', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=411510b583e6bc28de56ce41023bfb9c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=34afe99ae3f57cff8e01ccdbffaf75c8', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=74ea57684b830429946348b395270645', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=8ff8afc5b9a24145fe069dc4351159ed', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=140&quality=85&auto=format&fit=max&s=5a933a670ae63e32065d3689fbe22e0c', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=e06f67eb06795e98234cdf47377e64b2', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=120&quality=85&auto=format&fit=max&s=fb99ab71fd2eefac5fba695e37a1d10a', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=1704c83794f3f904f5fff69b982a3613', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=380&quality=85&auto=format&fit=max&s=0a23b3f6ff4740d06978d7daff67bf8c', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=08b0b6c0631e0b8656d58a150202fea4', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=300&quality=85&auto=format&fit=max&s=42968db09068a09b98adf9d6574c76f2', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=8c094c10ad862877a220e3c4314762ce', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=c43833d5667a6d16acca99b7c4873c96', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f44f8849f904da4a348b9d11d29b0928', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=411510b583e6bc28de56ce41023bfb9c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=34afe99ae3f57cff8e01ccdbffaf75c8', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=74ea57684b830429946348b395270645', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=8ff8afc5b9a24145fe069dc4351159ed', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=860&quality=85&auto=format&fit=max&s=56e2e03ae3e56014d44d0db1bd1bd81f', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=d3f47881f1b85665d4fc3aa0f453ccb6', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=780&quality=85&auto=format&fit=max&s=8cce7410ca55126bcecdd691ad21842d', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=d09f570f395fc93c3cbaeb295c15f202', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=c43833d5667a6d16acca99b7c4873c96', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f44f8849f904da4a348b9d11d29b0928', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=411510b583e6bc28de56ce41023bfb9c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=34afe99ae3f57cff8e01ccdbffaf75c8', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=74ea57684b830429946348b395270645', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=8ff8afc5b9a24145fe069dc4351159ed', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=c43833d5667a6d16acca99b7c4873c96', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f44f8849f904da4a348b9d11d29b0928', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=411510b583e6bc28de56ce41023bfb9c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=34afe99ae3f57cff8e01ccdbffaf75c8', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=74ea57684b830429946348b395270645', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=8ff8afc5b9a24145fe069dc4351159ed', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=1900&quality=85&auto=format&fit=max&s=9c98cecfae4ec5e18f84e06bc94aec0a', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=89ef757448e93c0a15a86cc22363b218', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=1300&quality=85&auto=format&fit=max&s=39f29d5109565048b22172487626a66d', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=974e0a832f9ab0c52ecbab44e4e81008', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=1140&quality=85&auto=format&fit=max&s=aa6070b9f138eb6ebe3b08f35b87b644', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=834ea3f6784f36b3e61c9443df0cb5e4', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=980&quality=85&auto=format&fit=max&s=ea25670bf574c198acef2beff787fa04', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=47711c285f7576a446a6bd41be250207', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=740&quality=85&auto=format&fit=max&s=ad631865759cb649acdd37bd37b9814d', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=dd69d58cda218a59232d063732a1f785', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=660&quality=85&auto=format&fit=max&s=2e9a5517f5561de226f00a236f08e021', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=52b117dd57d6c3652788f3962300021f', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=480&quality=85&auto=format&fit=max&s=5fa9b4d2901a306ff0fb8bc1fcfa430d', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/34f1a6b4eef3f8b47a4bfed09b987099cad24355/0_0_5000_4000/master/5000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=11afb608b1ba218910dacd2f902af97e', + width: 960, + }, + ], + }, + ], + data: { + alt: 'John Lewis Intentional Leather Medium Cross body Bag, Black', + credit: 'Photograph: John Lewis', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Cross-body bag

', + elementId: 'fde79c7f-81e8-4130-8ae3-45246790c53f', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.johnlewis.com%2Fjohn-lewis-intentional-leather-medium-crossbody-bag-black%2Fp114347477&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£79.20 at John Lewis', + elementId: '08c82e98-0f35-4b4b-b880-06983056563a', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

“Leather bags for under £100 are hard to come by on the high street,” wrote Melanie Wilkinson in our guide to 50 affordable spring fashion updates for women. “But John Lewis has come up trumps with this chic shoulder bag, complete with expensive-looking gold hardware.” It’s dropped even further in price since, securing its place as one of your favourites this month.

', + elementId: '59a5a72f-d9bc-4a55-b30f-498b22c54fba', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '3e56ecd3-fe4a-458c-a3dc-c509066378b2', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The versatile running shoes

', + elementId: 'fd288eb0-478a-4856-b5c1-cc87ccc0ae42', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '16:9', + height: '2813', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/5000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '16:9', + isMaster: 'true', + height: '2813', + width: '5000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '16:9', + height: '1125', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '16:9', + height: '563', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '16:9', + height: '281', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '16:9', + height: '79', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/140.jpg', + }, + ], + }, + elementId: '4fc1c5e8-9964-4baf-b5ea-551d2f567a82', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=b4bdbae33607c4bb284cbd656c3986f6', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=9164befe52e9509427fd07b8a6d32cb5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=6a6fece2e9273130d4ffe2f213894d0c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=dc8c115a092dd8aca546836a285f183a', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=64d81180e45d6a0360ed9ee3a1d3c2ed', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=64928563412166b6c25b897b522932fb', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=140&quality=85&auto=format&fit=max&s=787f9a3d65ca4464ab5e456c946f3bf7', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=d14fb2fff1e924c09736a9f4457dcd48', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=120&quality=85&auto=format&fit=max&s=e8c281be3b95b8b035ed51994400d9e5', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=befa8cf2ed5fa35b1b8eec08125fb66d', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=380&quality=85&auto=format&fit=max&s=fd76182b9a154d7041280f5609d4638c', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=a01fbedbfd0e4879893cc9c18b5effb9', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=300&quality=85&auto=format&fit=max&s=c28ca37e80bd2e286b82022b990279a9', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=e0038de07e30e02182943c8b551a8739', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=b4bdbae33607c4bb284cbd656c3986f6', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=9164befe52e9509427fd07b8a6d32cb5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=6a6fece2e9273130d4ffe2f213894d0c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=dc8c115a092dd8aca546836a285f183a', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=64d81180e45d6a0360ed9ee3a1d3c2ed', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=64928563412166b6c25b897b522932fb', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=860&quality=85&auto=format&fit=max&s=baa42ee88651c1ef4ef435d446fe6c94', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=aefb93f88f121b3f85bbb95324c09569', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=780&quality=85&auto=format&fit=max&s=d8edcd2edbcec1c6d892a2b549a87907', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=a3e526cd6b301051cd6b311e186e6bc6', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=b4bdbae33607c4bb284cbd656c3986f6', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=9164befe52e9509427fd07b8a6d32cb5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=6a6fece2e9273130d4ffe2f213894d0c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=dc8c115a092dd8aca546836a285f183a', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=64d81180e45d6a0360ed9ee3a1d3c2ed', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=64928563412166b6c25b897b522932fb', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=85&auto=format&fit=max&s=b4bdbae33607c4bb284cbd656c3986f6', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=9164befe52e9509427fd07b8a6d32cb5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=85&auto=format&fit=max&s=6a6fece2e9273130d4ffe2f213894d0c', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=dc8c115a092dd8aca546836a285f183a', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=85&auto=format&fit=max&s=64d81180e45d6a0360ed9ee3a1d3c2ed', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=64928563412166b6c25b897b522932fb', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=1900&quality=85&auto=format&fit=max&s=086caa70df9bebf89c3df06ed11a4550', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=7ac79d868be4b518d83e92af8cf20011', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=1300&quality=85&auto=format&fit=max&s=e0e01f1849b80ecd82061b6e1dfe233b', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=55f8c059619e795183f7f30e2751f593', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=1140&quality=85&auto=format&fit=max&s=7c4c206b1e9b3dc77fcac7a3fe062dc5', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=35a19e45100ed39bdde49709412b2d7c', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=980&quality=85&auto=format&fit=max&s=e5a57c387db1e7d39f11cb0ceee46fcd', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=51e6380e3a97fec739e17bedccc1b4a3', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=740&quality=85&auto=format&fit=max&s=f0f6998f23e1a1a5b43041812a2e6301', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=38af4c0428755c829c81d670aee1f7e4', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=660&quality=85&auto=format&fit=max&s=381e867a3d4dcc0b06a1f9aa142f597b', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=c53fb744a62c9b67024cfb8debd7dc66', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=480&quality=85&auto=format&fit=max&s=f863012d4da6f85084af2c7902dc7896', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/e6ae128b58a36294a45b705bb0e46773030f212d/0_594_5000_2813/master/5000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=a1add0d1aee6c6dcfbf59c47cdd41deb', + width: 960, + }, + ], + }, + ], + data: { + alt: 'ASICS Unisex MEGABLAST Running Shoes', + credit: 'Photograph: Asics', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Asics Megablast unisex running shoes

', + elementId: '03b8c7fd-5206-4c6d-bcc4-4a68c2aba4f0', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.amazon.co.uk%2FASICS-Unisex-MEGABLAST-Running-Shoes%2Fdp%2FB0FV7D9Q3N%2F&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: 'From £242.88 at Amazon', + elementId: 'aac82680-9caa-4fe2-87b7-5de53672806a', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

For our guide to the best running shoes, Kieran Alger ran at least 50km in each pair (small fry for a man who’s completed almost 70 marathons and many ultras). But it was his pick for the most versatile that were your favourites in the piece – not surprising since they’re “some of the best do-it-all shoes you can buy”, “a great pick for speed sessions, long tempo efforts, cruising long Sunday runs and easy recovery efforts”.

', + elementId: '858368f9-7d8c-4643-8714-b7ad2ff3ef10', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: 'efc98f0b-d9fc-4e12-bb70-edbc9eb42ad6', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

Anya Hindmarch’s favourite supplement

', + elementId: 'b5a3fb3c-db9b-4f12-8ec4-e56150121eba', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/4000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/140.jpg', + }, + ], + }, + elementId: 'aac68211-24ad-4699-921c-21c23bf62e1d', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=d470cd4fe6cce1b257ef14256a5d0adf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=39283d12adc54f6c39e2f1fe321aac2f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=d3be9768b6b6876409a56bc29f0e4944', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=90559f0b3b7538bb8aa6b88c926e82d9', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e2b512b4d57e10acd5ce9d866b21a0cc', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=af45705fbaaab1068000dbe5d45d7beb', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=140&quality=85&auto=format&fit=max&s=70c6d9eaba31471bd1e39820de49f6fe', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=62945bcfbecf7a3df08fa0871999fc35', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=120&quality=85&auto=format&fit=max&s=52ccf40fc6b2d9df06e5c12f69ab436d', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=2bcee686601319ec07185c6527e85f5e', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=380&quality=85&auto=format&fit=max&s=ffdf3afd011a1a9a9c79fbb48f1a8a11', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=ca6645b0cec210b121a76b3e19ed5e39', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=300&quality=85&auto=format&fit=max&s=ef56cd82695091cf83bc91b4f8405522', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=038988f5f093663b2eec2da60b34be1a', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=d470cd4fe6cce1b257ef14256a5d0adf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=39283d12adc54f6c39e2f1fe321aac2f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=d3be9768b6b6876409a56bc29f0e4944', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=90559f0b3b7538bb8aa6b88c926e82d9', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e2b512b4d57e10acd5ce9d866b21a0cc', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=af45705fbaaab1068000dbe5d45d7beb', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=860&quality=85&auto=format&fit=max&s=a4b088c5766502580ec5e56dc94224f4', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=6ebca69fdeff691ce92e3715afc57226', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=780&quality=85&auto=format&fit=max&s=6f6be427c000b3537be6cc9ebfe660a6', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=8dcdb6e2fae359c2a6af42128e7c65ca', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=d470cd4fe6cce1b257ef14256a5d0adf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=39283d12adc54f6c39e2f1fe321aac2f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=d3be9768b6b6876409a56bc29f0e4944', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=90559f0b3b7538bb8aa6b88c926e82d9', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e2b512b4d57e10acd5ce9d866b21a0cc', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=af45705fbaaab1068000dbe5d45d7beb', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=d470cd4fe6cce1b257ef14256a5d0adf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=39283d12adc54f6c39e2f1fe321aac2f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=d3be9768b6b6876409a56bc29f0e4944', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=90559f0b3b7538bb8aa6b88c926e82d9', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=e2b512b4d57e10acd5ce9d866b21a0cc', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=af45705fbaaab1068000dbe5d45d7beb', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=1900&quality=85&auto=format&fit=max&s=cac9650ad287388f8dcf3e9fba3376a5', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=b5f95dc70f3b86c4fed4697621114c7e', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=1300&quality=85&auto=format&fit=max&s=821d1891802821838adad392d26e10a2', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=db2176ee0aba0928b766151e46687fca', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=1140&quality=85&auto=format&fit=max&s=533cad8c2483cd89870e7f1556d6a5ff', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=7fc24ae69c8d0d4556b5f9531b6f0c5a', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=980&quality=85&auto=format&fit=max&s=2460c0b0e6e5f5949fc649f12277ddd0', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=4e3ceda9d646080d503071a27d5f62eb', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=740&quality=85&auto=format&fit=max&s=d265a48fe5dc2fb7f7ec5b9d7a770f99', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=789c677e1ad0d0144bc68acfea927db2', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=660&quality=85&auto=format&fit=max&s=1fe4991ff858cc6756a0d2868c6b086a', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=703c29c24407cff7c83c353afe9b4e9c', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=480&quality=85&auto=format&fit=max&s=8711faa4a97fd14cb801f16c80ff1a5c', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/66b7d15889413edd0609018653322e478f672794/500_0_4000_4000/master/4000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=f41475406fbcbf2f52102bf88f5d54d4', + width: 960, + }, + ], + }, + ], + data: { + alt: 'ainslie + ainslie DAY POWDER', + credit: 'Photograph: Ainslie + Ainslie', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Ainslie + Ainslie day powder, 28 serves

', + elementId: '06a6d327-4f3d-4700-ab51-dbfaae810355', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fhealf.com%2Fen-uk%2Fproducts%2Fainslie-ainslie-day-powder&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£85 at Healf', + elementId: 'ed1f1f5f-7b7f-4041-b884-908fae408da2', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

After the popularity of Ben Fogle’s favourite toothpaste a few months back, you’ve once again taken inspiration from a celebrity’s daily routine. When we asked designer Anya Hindmarch which purchase she regretted the most, she said: “I regret not buying Ainslie + Ainslie sleep and day powder earlier. Gamechanging.”

', + elementId: '1bf1e4e7-98f0-402b-b756-15a3c958b06c', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: 'a356873a-4833-481d-991b-c046f59397b1', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

The first-step cleanser

', + elementId: '2078b9d0-787f-4b73-973c-8fef7d140127', + }, + { + displayCredit: false, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/4000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '4000', + width: '4000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/2000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/1000.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/500.jpg', + }, + { + index: 5, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/140.jpg', + }, + ], + }, + elementId: 'e9d54712-f426-48b1-bcac-d30f17dd48fd', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=83afcf4438937d07df5c1f907829dccf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=26cf4774ce1d7b52bb574e2829f3649a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=f7c7d8fe8bffbfe8cc055e106212fd7e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=346aa62e50032b0d49cf7930e01c6ce6', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=f4f21f333f6eecb6714619d0aa2d8196', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0fa07add20d3c9f8e79a0cea1ed7a6bb', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=140&quality=85&auto=format&fit=max&s=ede4628ba91688911d3ad2b8c17a926f', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=c8bc573937d8c146f85bc1d4e6500e31', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=120&quality=85&auto=format&fit=max&s=e0b2551d69488b8889918fd9e780ce5b', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=b51a8fc47748ef373d0c956558eee992', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=380&quality=85&auto=format&fit=max&s=ee185614ab08351080331e2eba99454e', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=09a94f0507779a9a0d67e24175de08fd', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=300&quality=85&auto=format&fit=max&s=9a7da5b5400d3ef4791a57182ae77432', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=b826061a25d55cfd3c66892d54342da9', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=83afcf4438937d07df5c1f907829dccf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=26cf4774ce1d7b52bb574e2829f3649a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=f7c7d8fe8bffbfe8cc055e106212fd7e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=346aa62e50032b0d49cf7930e01c6ce6', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=f4f21f333f6eecb6714619d0aa2d8196', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0fa07add20d3c9f8e79a0cea1ed7a6bb', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=860&quality=85&auto=format&fit=max&s=b894566d77dbf2c6a2d683cd6ce36233', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=63564d9b7f770e04ee950c6eb118f661', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=780&quality=85&auto=format&fit=max&s=32789c1aeac05fee8070437c94e81289', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=078e25d8ac21d8ca73530e0ee7439568', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=83afcf4438937d07df5c1f907829dccf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=26cf4774ce1d7b52bb574e2829f3649a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=f7c7d8fe8bffbfe8cc055e106212fd7e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=346aa62e50032b0d49cf7930e01c6ce6', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=f4f21f333f6eecb6714619d0aa2d8196', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0fa07add20d3c9f8e79a0cea1ed7a6bb', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=85&auto=format&fit=max&s=83afcf4438937d07df5c1f907829dccf', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=26cf4774ce1d7b52bb574e2829f3649a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=85&auto=format&fit=max&s=f7c7d8fe8bffbfe8cc055e106212fd7e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=346aa62e50032b0d49cf7930e01c6ce6', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=85&auto=format&fit=max&s=f4f21f333f6eecb6714619d0aa2d8196', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=0fa07add20d3c9f8e79a0cea1ed7a6bb', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=1900&quality=85&auto=format&fit=max&s=3caa3dc86706b358df5486b94535be9a', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=55f79f429db911794a13b2fba0872f74', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=1300&quality=85&auto=format&fit=max&s=b666ece85c225c55f2377861f900e450', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=d042e49a1bbc2ee3f2b978c55124a098', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=1140&quality=85&auto=format&fit=max&s=241d21df797cebf935c641c5448a1d34', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=3ab396bd52d41cc4b24a5d75ecaeb000', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=980&quality=85&auto=format&fit=max&s=ed1f4bac02ea70c7ee45efe0251fb92e', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=90349b08aad11fa8dfc32a0e54eade9d', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=740&quality=85&auto=format&fit=max&s=ec2d43b07f33ca2eb6087d40332a6805', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=6043561aeb992ec74346e64f7429657a', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=660&quality=85&auto=format&fit=max&s=d16fac2a6e1ab8ba96ac1fcc1842a4c8', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=beadaa036f61a97969f92d9138e01d48', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=480&quality=85&auto=format&fit=max&s=56033aa163b64101078185a2657d46d4', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/2b0df8d1c24ad6b5a74315a2aa1d39c5c0c84d42/500_0_4000_4000/master/4000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=2af5634f4cb79aa62804bb3d8927d19f', + width: 960, + }, + ], + }, + ], + data: { + alt: 'haruharu wonder Black Rice Moisture Cleansing Oil 150ml', + credit: 'Photograph: Haruharu Wonder', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Haruharu Wonder black rice cleansing oil, 150ml

', + elementId: '982c63d7-0d90-49bb-962c-269f96000ed3', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1572903&url=https%3A%2F%2Fwww.amazon.co.uk%2FHaruharu-Moisture-Cleansing-Cleanser-Macadamia%2Fdp%2FB08W1ZHTL3&sref=https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026.json?dcr', + label: '£15.30 at Amazon', + elementId: '511a2a5e-3e01-4831-92b6-c1708185c529', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Modern skincare is a confusing business. So we loved Anita Bhagwandas’s back-to-basics guide on how to build a skincare routine. Double cleansing is “the best way to cleanse your skin thoroughly in the evening,” she wrote, and this “brilliant” oil-based cleanser from Korean brand Haruharu Wonder is the ideal first step.

', + elementId: '3c9d55f4-1268-4ecd-b697-4dc068fc1a64', + }, + ], + attributes: { + pinned: false, + keyEvent: false, + summary: false, + }, + blockCreatedOn: 1777903225000, + blockCreatedOnDisplay: '15.00 BST', + blockLastUpdated: 1777631000000, + blockLastUpdatedDisplay: '11.23 BST', + contributors: [], + primaryDateLine: 'Mon 4 May 2026 15.00 BST', + secondaryDateLine: 'Last modified on Tue 5 May 2026 12.06 BST', + }, + ], + author: { + byline: 'Monica Horridge', + }, + byline: 'Monica Horridge', + webPublicationDate: '2026-05-04T14:00:25.000Z', + webPublicationDateDeprecated: '2026-05-04T14:00:25.000Z', + webPublicationDateDisplay: 'Mon 4 May 2026 15.00 BST', + webPublicationSecondaryDateDisplay: + 'Last modified on Tue 5 May 2026 12.06 BST', + editionLongForm: 'UK edition', + editionId: 'UK', + pageId: 'thefilter/2026/may/04/things-you-loved-most-april-2026', + canonicalUrl: + 'https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + format: { + design: 'FeatureDesign', + theme: 'LifestylePillar', + display: 'ShowcaseDisplay', + }, + designType: 'Feature', + tags: [ + { + id: 'thefilter/series/the-filter', + type: 'Series', + title: 'The Filter', + }, + { + id: 'lifeandstyle/lifeandstyle', + type: 'Keyword', + title: 'Life and style', + }, + { + id: 'campaign/email/the-filter', + type: 'Campaign', + title: 'The Filter (newsletter signup)', + }, + { + id: 'thefilter/series/the-filter-newsletter', + type: 'Series', + title: 'The Filter UK newsletter', + }, + { + id: 'tone/features', + type: 'Tone', + title: 'Features', + }, + { + id: 'type/article', + type: 'Type', + title: 'Article', + }, + { + id: 'tone/best-ofs', + type: 'Tone', + title: 'Best of', + }, + { + id: 'profile/monica-horridge', + type: 'Contributor', + title: 'Monica Horridge', + bylineImageUrl: + 'https://i.guim.co.uk/img/uploads/2025/05/20/Monica_Horridge.jpg?width=300&quality=85&auto=format&fit=max&s=336b53139e2631adf4ee3d6f0ad06a6c', + bylineLargeImageUrl: + 'https://i.guim.co.uk/img/uploads/2025/05/20/Monica_Horridge,_L.png?width=300&quality=85&auto=format&fit=max&s=903786d8d91b3e7698221103272d707e', + }, + { + id: 'tracking/commissioningdesk/the-filter', + type: 'Tracking', + title: 'The Filter UK', + }, + { + id: 'tracking/commissioningdesk/newsletters', + type: 'Tracking', + title: 'Newsletters', + }, + ], + pillar: 'lifestyle', + isLegacyInteractive: false, + isImmersive: false, + sectionLabel: 'Life and style', + sectionUrl: 'lifeandstyle/lifeandstyle', + sectionName: 'thefilter', + subMetaSectionLinks: [ + { + url: '/lifeandstyle/lifeandstyle', + title: 'Life and style', + }, + { + url: '/thefilter/series/the-filter', + title: 'The Filter', + }, + ], + subMetaKeywordLinks: [ + { + url: '/tone/features', + title: 'features', + }, + ], + shouldHideAds: false, + isAdFreeUser: false, + webURL: 'https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + linkedData: [ + { + '@type': 'NewsArticle', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + publisher: { + '@type': 'Organization', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com#publisher', + name: 'The Guardian', + url: 'https://www.theguardian.com/', + logo: { + '@type': 'ImageObject', + url: 'https://uploads.guim.co.uk/2018/01/31/TheGuardian_AMP.png', + width: 190, + height: 60, + }, + sameAs: [ + 'https://www.facebook.com/theguardian', + 'https://twitter.com/guardian', + 'https://www.youtube.com/user/TheGuardian', + ], + }, + isAccessibleForFree: true, + isPartOf: { + '@type': ['CreativeWork', 'Product'], + name: 'The Guardian', + productID: 'theguardian.com:basic', + }, + image: [ + 'https://i.guim.co.uk/img/media/762fe70aff6115264501c2b92a25476797d40ba3/286_86_4530_3624/master/4530.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvZmlsdGVyLXVrLnBuZw&enable=upscale&s=47b5ebe78c968b6130b687e1fc1e194b', + 'https://i.guim.co.uk/img/media/762fe70aff6115264501c2b92a25476797d40ba3/286_86_4530_3624/master/4530.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=45d61f33c04fd715d71e7137cd974704', + 'https://i.guim.co.uk/img/media/762fe70aff6115264501c2b92a25476797d40ba3/286_86_4530_3624/master/4530.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=86d4d124e33879df82326849ed1b0732', + 'https://i.guim.co.uk/img/media/762fe70aff6115264501c2b92a25476797d40ba3/286_86_4530_3624/master/4530.jpg?width=1200&quality=85&auto=format&fit=max&s=ab8f86b18f83e8f48ad2ac9f21339ccf', + ], + author: [ + { + '@type': 'Person', + name: 'Monica Horridge', + sameAs: 'https://www.theguardian.com/profile/monica-horridge', + }, + ], + datePublished: '2026-05-04T14:00:25.000Z', + headline: + 'From skin-brightening serum to a bargain coffee machine: 10 things you loved most in April', + dateModified: '2026-05-05T11:06:43.000Z', + mainEntityOfPage: + 'https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + { + '@type': 'WebPage', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + potentialAction: { + '@type': 'ViewAction', + target: 'android-app://com.guardian/https/www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + }, + ], + openGraphData: { + 'og:url': + 'https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + 'article:author': 'https://www.theguardian.com/profile/monica-horridge', + 'og:image:width': '1200', + 'og:image': + 'https://i.guim.co.uk/img/media/762fe70aff6115264501c2b92a25476797d40ba3/286_86_4530_3624/master/4530.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvZmlsdGVyLXVrLnBuZw&enable=upscale&s=47b5ebe78c968b6130b687e1fc1e194b', + 'al:ios:url': + 'gnmguardian://thefilter/2026/may/04/things-you-loved-most-april-2026?contenttype=Article&source=applinks', + 'article:publisher': 'https://www.facebook.com/theguardian', + 'og:title': + 'From skin-brightening serum to a bargain coffee machine: 10 things you loved most in April', + 'fb:app_id': '180444840287', + 'article:modified_time': '2026-05-05T11:06:43.000Z', + 'og:image:height': '960', + 'og:description': + 'Whether it’s a new season scent or a springy running shoe, your April favourites show you’re ready for a fresh start', + 'og:type': 'article', + 'al:ios:app_store_id': '409128287', + 'article:section': 'The Filter', + 'article:published_time': '2026-05-04T14:00:25.000Z', + 'article:tag': 'Life and style', + 'al:ios:app_name': 'The Guardian', + 'og:site_name': 'the Guardian', + }, + twitterData: { + 'twitter:app:id:iphone': '409128287', + 'twitter:app:name:googleplay': 'The Guardian', + 'twitter:app:name:ipad': 'The Guardian', + 'twitter:card': 'summary_large_image', + 'twitter:app:name:iphone': 'The Guardian', + 'twitter:app:id:ipad': '409128287', + 'twitter:app:id:googleplay': 'com.guardian', + 'twitter:app:url:googleplay': + 'guardian://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + 'twitter:app:url:iphone': + 'gnmguardian://thefilter/2026/may/04/things-you-loved-most-april-2026?contenttype=Article&source=twitter', + 'twitter:image': + 'https://i.guim.co.uk/img/media/762fe70aff6115264501c2b92a25476797d40ba3/286_86_4530_3624/master/4530.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvZmlsdGVyLXVrLnBuZw&s=f60705aa0242d7abe143cf99da9383eb', + 'twitter:site': '@guardian', + 'twitter:app:url:ipad': + 'gnmguardian://thefilter/2026/may/04/things-you-loved-most-april-2026?contenttype=Article&source=twitter', + }, + config: { + references: [ + { + 'rich-link': + 'https://www.theguardian.com/environment/2015/oct/19/sign-up-to-the-green-light-email', + }, + ], + shortUrlId: '/p/d8ex5', + switches: { + prebidAppnexusUkRow: true, + clickToView: true, + prebidTrustx: true, + scAdFreeBanner: false, + compareVariantDecision: false, + enableSentryReporting: true, + lazyLoadContainers: true, + adFreeStrictExpiryEnforcement: false, + liveblogRendering: true, + remarketing: true, + registerWithPhone: false, + targeting: true, + extendedMostPopularFronts: true, + slotBodyEnd: true, + emailInlineInFooter: true, + facebookTrackingPixel: true, + serviceWorkerEnabled: false, + iasAdTargeting: true, + extendedMostPopular: true, + prebidAnalytics: true, + imrWorldwide: true, + acast: true, + twitterUwt: true, + prebidAppnexusInvcode: true, + prebidAppnexus: true, + enableDiscussionSwitch: true, + prebidXaxis: true, + interactiveFullHeaderSwitch: false, + discussionAllPageSize: true, + prebidUserSync: true, + audioOnwardJourneySwitch: true, + mobileStickyPrebid: true, + breakingNews: true, + externalVideoEmbeds: true, + carrotTrafficDriver: true, + geoMostPopular: true, + weAreHiring: true, + relatedContent: true, + thirdPartyEmbedTracking: true, + prebidOzone: true, + mostViewedFronts: true, + abSignInGateMainControl: true, + ampPrebid: true, + googleSearch: true, + brazeSwitch: true, + consentManagement: true, + commercial: true, + prebidSonobi: true, + idProfileNavigation: true, + confiantAdVerification: true, + discussionAllowAnonymousRecommendsSwitch: false, + scrollDepth: true, + permutive: true, + comscore: true, + webFonts: true, + prebidImproveDigital: true, + ophan: true, + crosswordSvgThumbnails: true, + prebidTriplelift: true, + weather: true, + commercialOutbrainNewids: true, + dotcomRendering: true, + abSignInGateMainVariant: true, + hostedVideoAutoplay: true, + abAdblockAsk: true, + prebidPubmatic: true, + autoRefresh: true, + enhanceTweets: true, + prebidIndexExchange: true, + prebidOpenx: true, + idCookieRefresh: true, + sharingComments: true, + abSignInGateMandatory: true, + discussionPageSize: true, + smartAppBanner: false, + boostGaUserTimingFidelity: false, + historyTags: true, + mobileStickyLeaderboard: true, + abDeeplyReadTest: false, + surveys: true, + remoteBanner: true, + inizio: true, + prebidHeaderBidding: true, + a9HeaderBidding: true, + lightbox: true, + }, + keywordIds: + 'environment/climate-change,environment/environment,science/scienceofclimatechange,science/science,world/eu,world/europe-news,world/world,environment/flooding,world/wildfires,world/natural-disasters', + sharedAdTargeting: { + ct: 'article', + co: ['jennifer-rankin'], + url: '/environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + su: ['0'], + edition: 'uk', + tn: ['news'], + p: 'ng', + k: [ + 'eu', + 'flooding', + 'world', + 'europe-news', + 'natural-disasters', + 'science', + 'environment', + 'climate-change', + 'wildfires', + 'scienceofclimatechange', + ], + sh: 'https://www.theguardian.com/p/d8ex5', + }, + toneIds: 'tone/news', + dcrSentryDsn: + 'https://1937ab71c8804b2b8438178dfdd6468f@sentry.io/1377847', + discussionApiUrl: 'https://discussion.theguardian.com/discussion-api', + sentryPublicApiKey: '344003a8d11c41d8800fbad8383fdc50', + commercialBundleUrl: + 'https://assets.guim.co.uk/javascripts/bc58c17d75809551440f/graun.commercial.dcr.js', + discussionApiClientHeader: 'nextgen', + shouldHideReaderRevenue: false, + sentryHost: 'app.getsentry.com/35463', + isPaidContent: false, + headline: 'Headline string', + idApiUrl: 'https://idapi.theguardian.com', + showRelatedContent: true, + adUnit: '/59666047/theguardian.com/environment/article/ng', + videoDuration: 0, + stage: 'PROD', + isSensitive: false, + isDev: false, + ajaxUrl: 'https://api.nextgen.guardianapps.co.uk', + keywords: + 'Climate change,Environment,Climate change,Science,European Union,Europe,World news,Flooding,Wildfires,Natural disasters and extreme weather', + revisionNumber: 'DEV', + section: 'environment', + isPhotoEssay: false, + ampIframeUrl: + 'https://assets.guim.co.uk/data/vendor/b242a49b1588bb36bdaacefe001ca77a/amp-iframe.html', + isLive: false, + host: 'https://www.theguardian.com', + brazeApiKey: '7f28c639-8bda-48ff-a3f6-24345abfc07c', + contentType: 'Article', + idUrl: 'https://profile.theguardian.com', + author: 'Jennifer Rankin', + dfpAccountId: '59666047', + pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', + mmaUrl: 'https://manage.theguardian.com', + serverSideABTests: {}, + edition: 'UK', + ipsosTag: 'environment', + isLiveBlog: false, + frontendAssetsFullURL: 'https://assets.guim.co.uk/', + webPublicationDate: 1581314427000, + discussionD2Uid: 'zHoBy6HNKsk', + }, + guardianBaseURL: 'https://www.theguardian.com', + contentType: 'Article', + hasRelated: true, + hasStoryPackage: false, + beaconURL: '//phar.gu-web.net', + isCommentable: false, + commercialProperties: { + US: { + adTargeting: [ + { + name: 'sh', + value: 'https://www.theguardian.com/p/x5vtf5', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'edition', + value: 'us', + }, + { + name: 'co', + value: ['monica-horridge'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter', + }, + { + name: 'se', + value: ['the-filter-newsletter', 'the-filter'], + }, + { + name: 'url', + value: '/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'tn', + value: ['features', 'best-ofs'], + }, + ], + }, + AU: { + adTargeting: [ + { + name: 'sh', + value: 'https://www.theguardian.com/p/x5vtf5', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'edition', + value: 'au', + }, + { + name: 'co', + value: ['monica-horridge'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter', + }, + { + name: 'se', + value: ['the-filter-newsletter', 'the-filter'], + }, + { + name: 'url', + value: '/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'tn', + value: ['features', 'best-ofs'], + }, + ], + }, + UK: { + adTargeting: [ + { + name: 'sh', + value: 'https://www.theguardian.com/p/x5vtf5', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter', + }, + { + name: 'se', + value: ['the-filter-newsletter', 'the-filter'], + }, + { + name: 'url', + value: '/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'tn', + value: ['features', 'best-ofs'], + }, + { + name: 'co', + value: ['monica-horridge'], + }, + { + name: 'edition', + value: 'uk', + }, + ], + }, + INT: { + adTargeting: [ + { + name: 'sh', + value: 'https://www.theguardian.com/p/x5vtf5', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'co', + value: ['monica-horridge'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter', + }, + { + name: 'se', + value: ['the-filter-newsletter', 'the-filter'], + }, + { + name: 'url', + value: '/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + { + name: 'edition', + value: 'int', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'tn', + value: ['features', 'best-ofs'], + }, + ], + }, + EUR: { + adTargeting: [ + { + name: 'sh', + value: 'https://www.theguardian.com/p/x5vtf5', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'co', + value: ['monica-horridge'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter', + }, + { + name: 'se', + value: ['the-filter-newsletter', 'the-filter'], + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'tn', + value: ['features', 'best-ofs'], + }, + { + name: 'url', + value: '/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + { + name: 'edition', + value: 'eur', + }, + ], + }, + }, + pageType: { + hasShowcaseMainElement: true, + isFront: false, + isLiveblog: false, + isMinuteArticle: false, + isPaidContent: false, + isPreview: false, + isSensitive: false, + }, + trailText: + 'Whether it’s a new season scent or a springy running shoe, your April favourites show you’re ready for a fresh start', + nav: { + currentUrl: '/thefilter', + pillars: [ + { + title: 'News', + url: '/', + longTitle: 'Headlines', + iconName: 'home', + children: [ + { + title: 'UK', + url: '/uk-news', + longTitle: 'UK news', + children: [ + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Education', + url: '/education', + children: [ + { + title: 'Schools', + url: '/education/schools', + }, + { + title: 'Teachers', + url: '/teacher-network', + }, + { + title: 'Universities', + url: '/education/universities', + }, + { + title: 'Students', + url: '/education/students', + }, + ], + }, + { + title: 'Media', + url: '/media', + }, + { + title: 'Society', + url: '/society', + }, + { + title: 'Law', + url: '/law', + }, + { + title: 'Scotland', + url: '/uk/scotland', + }, + { + title: 'Wales', + url: '/uk/wales', + }, + { + title: 'Northern Ireland', + url: '/uk/northernireland', + }, + ], + }, + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'US politics', + url: '/us-news/us-politics', + }, + { + title: 'World', + url: '/world', + longTitle: 'World news', + children: [ + { + title: 'Europe', + url: '/world/europe-news', + }, + { + title: 'US news', + url: '/us-news', + longTitle: 'US news', + }, + { + title: 'Americas', + url: '/world/americas', + }, + { + title: 'Asia', + url: '/world/asia', + }, + { + title: 'Australia', + url: '/australia-news', + longTitle: 'Australia news', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Africa', + url: '/world/africa', + }, + { + title: 'Inequality', + url: '/inequality', + }, + { + title: 'Global development', + url: '/global-development', + }, + ], + }, + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Ukraine', + url: '/world/ukraine', + }, + { + title: 'Football', + url: '/football', + children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: 'Business', + url: '/business', + children: [ + { + title: 'Economics', + url: '/business/economics', + }, + { + title: 'Banking', + url: '/business/banking', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Markets', + url: '/business/stock-markets', + }, + { + title: 'Project Syndicate', + url: '/business/series/project-syndicate-economists', + }, + { + title: 'B2B', + url: '/business-to-business', + }, + { + title: 'Retail', + url: '/business/retail', + }, + ], + }, + { + title: 'Environment', + url: '/environment', + children: [ + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Wildlife', + url: '/environment/wildlife', + }, + { + title: 'Energy', + url: '/environment/energy', + }, + { + title: 'Pollution', + url: '/environment/pollution', + }, + ], + }, + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Science', + url: '/science', + }, + { + title: 'Tech', + url: '/technology', + }, + { + title: 'Global development', + url: '/global-development', + }, + { + title: 'Obituaries', + url: '/obituaries', + }, + ], + }, + { + title: 'Opinion', + url: '/commentisfree', + longTitle: 'Opinion home', + iconName: 'home', + children: [ + { + title: 'The Guardian view', + url: '/profile/editorial', + }, + { + title: 'Columnists', + url: '/uk/columnists', + }, + { + title: 'Cartoons', + url: '/tone/cartoons', + }, + { + title: 'Opinion videos', + url: '/type/video+tone/comment', + }, + { + title: 'Letters', + url: '/tone/letters', + }, + ], + }, + { + title: 'Sport', + url: '/sport', + longTitle: 'Sport home', + iconName: 'home', + children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'Football', + url: '/football', + children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Cricket', + url: '/sport/cricket', + }, + { + title: 'Rugby union', + url: '/sport/rugby-union', + }, + { + title: 'Tennis', + url: '/sport/tennis', + }, + { + title: 'Cycling', + url: '/sport/cycling', + }, + { + title: 'F1', + url: '/sport/formulaone', + }, + { + title: 'Golf', + url: '/sport/golf', + }, + { + title: 'Boxing', + url: '/sport/boxing', + }, + { + title: 'Rugby league', + url: '/sport/rugbyleague', + }, + { + title: 'Racing', + url: '/sport/horse-racing', + }, + { + title: 'US sports', + url: '/sport/us-sport', + }, + ], + }, + { + title: 'Culture', + url: '/culture', + longTitle: 'Culture home', + iconName: 'home', + children: [ + { + title: 'Film', + url: '/film', + }, + { + title: 'Music', + url: '/music', + }, + { + title: 'TV & radio', + url: '/tv-and-radio', + }, + { + title: 'Books', + url: '/books', + }, + { + title: 'Art & design', + url: '/artanddesign', + }, + { + title: 'Stage', + url: '/stage', + }, + { + title: 'Games', + url: '/games', + }, + { + title: 'Classical', + url: '/music/classicalmusicandopera', + }, + ], + }, + { + title: 'Lifestyle', + url: '/lifeandstyle', + longTitle: 'Lifestyle home', + iconName: 'home', + children: [ + { + title: 'The Filter', + url: '/uk/thefilter', + }, + { + title: 'Fashion', + url: '/fashion', + }, + { + title: 'Food', + url: '/food', + }, + { + title: 'Recipes', + url: '/tone/recipes', + }, + { + title: 'Travel', + url: '/travel', + children: [ + { + title: 'UK', + url: '/travel/uk', + }, + { + title: 'Europe', + url: '/travel/europe', + }, + { + title: 'US', + url: '/travel/usa', + }, + ], + }, + { + title: 'Health & fitness', + url: '/lifeandstyle/health-and-wellbeing', + }, + { + title: 'Women', + url: '/lifeandstyle/women', + }, + { + title: 'Men', + url: '/lifeandstyle/men', + }, + { + title: 'Love & sex', + url: '/lifeandstyle/love-and-sex', + }, + { + title: 'Beauty', + url: '/fashion/beauty', + }, + { + title: 'Home & garden', + url: '/lifeandstyle/home-and-garden', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Cars', + url: '/technology/motoring', + }, + ], + }, + ], + otherLinks: [ + { + title: 'The Guardian app', + url: 'https://app.adjust.com/16xt6hai', + }, + { + title: 'Video', + url: '/video', + }, + { + title: 'Podcasts', + url: '/podcasts', + }, + { + title: 'Pictures', + url: '/inpictures', + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: "Today's paper", + url: '/theguardian', + children: [ + { + title: 'Obituaries', + url: '/obituaries', + }, + { + title: 'G2', + url: '/theguardian/g2', + }, + { + title: 'Journal', + url: '/theguardian/journal', + }, + { + title: 'Saturday', + url: '/theguardian/saturday', + }, + ], + }, + { + title: 'Inside the Guardian', + url: 'https://www.theguardian.com/insidetheguardian', + }, + { + title: 'Guardian Weekly', + url: 'https://www.theguardian.com/weekly?INTCMP=gdnwb_mawns_editorial_gweekly_GW_TopNav_UK', + }, + { + title: 'Crosswords', + url: '/crosswords', + children: [ + { + title: 'Blog', + url: '/crosswords/crossword-blog', + }, + { + title: 'Quick', + url: '/crosswords/series/quick', + }, + { + title: 'Sunday quick', + url: '/crosswords/series/sunday-quick', + }, + { + title: 'Mini', + url: '/crosswords/series/mini-crossword', + }, + { + title: 'Quick cryptic', + url: '/crosswords/series/quick-cryptic', + }, + { + title: 'Quiptic', + url: '/crosswords/series/quiptic', + }, + { + title: 'Cryptic', + url: '/crosswords/series/cryptic', + }, + { + title: 'Prize', + url: '/crosswords/series/prize', + }, + { + title: 'Genius', + url: '/crosswords/series/genius', + }, + { + title: 'Weekend', + url: '/crosswords/series/weekend-crossword', + }, + { + title: 'Special', + url: '/crosswords/series/special', + }, + ], + }, + { + title: 'Wordiply', + url: 'https://www.wordiply.com', + }, + { + title: 'Corrections', + url: '/theguardian/series/corrections-and-clarifications', + }, + { + title: 'Tips', + url: 'https://www.theguardian.com/tips', + }, + ], + brandExtensions: [ + { + title: 'Search jobs', + url: 'https://jobs.theguardian.com', + }, + { + title: 'Hire with Guardian Jobs', + url: 'https://recruiters.theguardian.com/?utm_source=gdnwb&utm_medium=navbar&utm_campaign=Guardian_Navbar_Recruiters&CMP_TU=trdmkt&CMP_BUNIT=jobs', + }, + { + title: 'Holidays', + url: 'https://holidays.theguardian.com?INTCMP=holidays_uk_web_newheader', + }, + { + title: 'Live events', + url: 'https://www.theguardian.com/guardian-live-events?INTCMP=live_uk_header_dropdown', + }, + { + title: 'About Us', + url: '/about', + }, + { + title: 'Digital Archive', + url: 'https://theguardian.newspapers.com', + }, + { + title: 'Guardian Print Shop', + url: '/artanddesign/series/gnm-print-sales', + }, + { + title: 'Patrons', + url: 'https://patrons.theguardian.com/?INTCMP=header_patrons', + }, + { + title: 'Guardian Licensing', + url: 'https://licensing.theguardian.com/', + }, + ], + readerRevenueLinks: { + header: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + footer: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_supporter_cta%22%7D', + }, + sideMenu: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=side_menu_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=side_menu_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=side_menu_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=mobilenav_print_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22mobilenav_print_cta%22%7D', + }, + ampHeader: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=amp_header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22amp_header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + ampFooter: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=amp_footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_supporter_cta%22%7D', + }, + }, + }, + showBottomSocialButtons: true, + pageFooter: { + footerLinks: [ + [ + { + text: 'About us', + url: '/about', + dataLinkName: 'uk : footer : about us', + extraClasses: '', + }, + { + text: 'Help', + url: 'https://manage.theguardian.com/help-centre', + dataLinkName: 'uk : footer : tech feedback', + extraClasses: 'js-tech-feedback-report', + }, + { + text: 'Complaints & corrections', + url: '/info/complaints-and-corrections', + dataLinkName: 'complaints', + extraClasses: '', + }, + { + text: 'Contact us', + url: '/help/contact-us', + dataLinkName: 'uk : footer : contact us', + extraClasses: '', + }, + { + text: 'Tip us off', + url: 'https://www.theguardian.com/tips', + dataLinkName: 'uk : footer : tips', + extraClasses: '', + }, + { + text: 'SecureDrop', + url: 'https://www.theguardian.com/securedrop', + dataLinkName: 'securedrop', + extraClasses: '', + }, + { + text: 'Privacy policy', + url: '/info/privacy', + dataLinkName: 'privacy', + extraClasses: '', + }, + { + text: 'Cookie policy', + url: '/info/cookies', + dataLinkName: 'cookie', + extraClasses: '', + }, + { + text: 'Modern Slavery Act', + url: 'https://uploads.guim.co.uk/2025/09/05/Modern_Slavery_Statement_2025.pdf', + dataLinkName: 'uk : footer : modern slavery act statement', + extraClasses: '', + }, + { + text: 'Tax strategy', + url: 'https://uploads.guim.co.uk/2025/09/05/Tax_strategy_for_the_year_ended_31_March_2025.pdf', + dataLinkName: 'uk : footer : tax strategy', + extraClasses: '', + }, + { + text: 'Terms & conditions', + url: '/help/terms-of-service', + dataLinkName: 'terms', + extraClasses: '', + }, + ], + [ + { + text: 'All topics', + url: '/index/subjects/a', + dataLinkName: 'uk : footer : all topics', + extraClasses: '', + }, + { + text: 'All writers', + url: '/index/contributors', + dataLinkName: 'uk : footer : all contributors', + extraClasses: '', + }, + { + text: 'Newsletters', + url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', + dataLinkName: 'uk : footer : newsletters', + extraClasses: '', + }, + { + text: 'Digital newspaper archive', + url: 'https://theguardian.newspapers.com', + dataLinkName: 'digital newspaper archive', + extraClasses: '', + }, + { + text: 'Bluesky', + url: 'https://bsky.app/profile/theguardian.com', + dataLinkName: 'uk : footer : Bluesky', + extraClasses: '', + }, + { + text: 'Facebook', + url: 'https://www.facebook.com/theguardian', + dataLinkName: 'uk : footer : Facebook', + extraClasses: '', + }, + { + text: 'Instagram', + url: 'https://www.instagram.com/guardian', + dataLinkName: 'uk : footer : Instagram', + extraClasses: '', + }, + { + text: 'LinkedIn', + url: 'https://www.linkedin.com/company/theguardian', + dataLinkName: 'uk : footer : LinkedIn', + extraClasses: '', + }, + { + text: 'Threads', + url: 'https://www.threads.com/@guardian', + dataLinkName: 'uk : footer : Threads', + extraClasses: '', + }, + { + text: 'TikTok', + url: 'https://www.tiktok.com/@guardian', + dataLinkName: 'uk : footer : TikTok', + extraClasses: '', + }, + { + text: 'YouTube', + url: 'https://www.youtube.com/user/TheGuardian', + dataLinkName: 'uk : footer : YouTube', + extraClasses: '', + }, + ], + [ + { + text: 'Advertise with us', + url: 'https://advertising.theguardian.com', + dataLinkName: 'uk : footer : advertise with us', + extraClasses: '', + }, + { + text: 'Guardian Labs', + url: '/guardian-labs', + dataLinkName: 'uk : footer : guardian labs', + extraClasses: '', + }, + { + text: 'Search jobs', + url: 'https://jobs.theguardian.com', + dataLinkName: 'uk : footer : jobs', + extraClasses: '', + }, + { + text: 'Patrons', + url: 'https://patrons.theguardian.com?INTCMP=footer_patrons', + dataLinkName: 'uk : footer : patrons', + extraClasses: '', + }, + { + text: 'Work with us', + url: 'https://workwithus.theguardian.com/', + dataLinkName: 'uk : footer : work with us', + extraClasses: '', + }, + { + text: 'Accessibility settings', + url: '/help/accessibility-help', + dataLinkName: 'accessibility settings', + extraClasses: '', + }, + ], + ], + }, + publication: 'theguardian.com', + shouldHideReaderRevenue: false, + slotMachineFlags: '', + contributionsServiceUrl: 'https://contributions.guardianapis.com', + isSpecialReport: false, + promotedNewsletter: { + identityName: 'the-filter', + name: 'The Filter', + theme: 'lifestyle', + description: + 'Get the best shopping advice from the Filter team straight to your inbox. The Guardian’s journalism is independent. We will earn a commission if you buy something through an affiliate link.', + frequency: 'Weekly', + listId: 6050, + group: 'Lifestyle', + successDescription: 'We’ll send you the Filter every Sunday', + regionFocus: 'UK', + illustrationCard: + 'https://uploads.guim.co.uk/2024/10/04/The_Filter_-_5-3.jpg', + illustrationSquare: + 'https://media.guim.co.uk/c8a44e1d3ffcbf49017e464b41e9c7b31139e713/926_0_379_379/379.jpg', + exampleUrl: '/thefilter/series/the-filter-newsletter/latest', + category: 'article-based', + }, + showTableOfContents: false, + lang: 'en', + isRightToLeftLang: false, +}; diff --git a/dotcom-rendering/fixtures/generated/fe-articles/AffiliateProductStandard.ts b/dotcom-rendering/fixtures/generated/fe-articles/AffiliateProductStandard.ts new file mode 100644 index 00000000000..551f8ed42df --- /dev/null +++ b/dotcom-rendering/fixtures/generated/fe-articles/AffiliateProductStandard.ts @@ -0,0 +1,4123 @@ +/** + * DO NOT EDIT THIS FILE! + * + * This file was automatically generated using the gen-fixtures.js script. Any edits + * you make here will be lost. + * + * If the data in these fixtures is not what you expect then + * + * 1. Refresh the data using 'make gen-fixtures' or + * 2. if the latest live data is not what you need, then consider editing + * gen-fixtures.js directly. + */ + +import type { FEArticle } from '../../../src/frontend/feArticle'; + +export const AffiliateProductStandard: FEArticle = { + version: 3, + headline: + 'The Anyday glass food containers transformed how I store leftovers', + standfirst: + '

Don’t let leftovers languish in the fridge. These oven, microwave and freezer-safe glass containers from Anyday helped me waste less food – and do fewer dishes

\n', + webTitle: + 'Less waste and fewer dishes: these glass food containers changed how I store leftovers', + affiliateLinksDisclaimer: 'true', + mainMediaElements: [ + { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '5:4', + height: '800', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/1000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '5:4', + height: '400', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/500.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '5:4', + height: '112', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/140.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '5:4', + height: '1382', + width: '1728', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/1728.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '5:4', + isMaster: 'true', + height: '1382', + width: '1728', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg', + }, + ], + }, + elementId: 'b6f5c26c-534a-477f-91b6-1d5868ab740a', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=620&quality=85&auto=format&fit=max&s=ae7349e98597c75dd6be5887fabe0be5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=2a5fc93827a828b8b16efde2a0399956', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=700&quality=85&auto=format&fit=max&s=a08ff735cc8eaf8dd3f660a745b3e5ea', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=e0fd509035e3ef636da27abdb1e3c090', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=620&quality=85&auto=format&fit=max&s=ae7349e98597c75dd6be5887fabe0be5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=2a5fc93827a828b8b16efde2a0399956', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=645&quality=85&auto=format&fit=max&s=6e9572bb560f8f3d24fbd307765932ee', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=f41374d104978e5b53e28064d262af01', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=465&quality=85&auto=format&fit=max&s=04c3e6ed7a8eb65acbb4427aba95a36e', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=a83822e43cb8b2e5138c64d9b703667b', + width: 930, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [], + }, + { + weighting: 'supporting', + srcSet: [], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1020&quality=85&auto=format&fit=max&s=c27794240a8c0239aae5bf8adbb33dee', + width: 1020, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1020&quality=45&auto=format&fit=max&dpr=2&s=8791207f69f257c6af240f67095194dd', + width: 2040, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=940&quality=85&auto=format&fit=max&s=5bd9008b974758c359ee18b628da45c6', + width: 940, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=940&quality=45&auto=format&fit=max&dpr=2&s=d59556f68f0d22783635ecb5b5ca19af', + width: 1880, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=700&quality=85&auto=format&fit=max&s=a08ff735cc8eaf8dd3f660a745b3e5ea', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=e0fd509035e3ef636da27abdb1e3c090', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=700&quality=85&auto=format&fit=max&s=a08ff735cc8eaf8dd3f660a745b3e5ea', + width: 700, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=700&quality=45&auto=format&fit=max&dpr=2&s=e0fd509035e3ef636da27abdb1e3c090', + width: 1400, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=660&quality=85&auto=format&fit=max&s=aa8664c687af48eb4315aa7178de067e', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=d248dbebd46826663fcd653860f615f2', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=645&quality=85&auto=format&fit=max&s=6e9572bb560f8f3d24fbd307765932ee', + width: 645, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=645&quality=45&auto=format&fit=max&dpr=2&s=f41374d104978e5b53e28064d262af01', + width: 1290, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=465&quality=85&auto=format&fit=max&s=04c3e6ed7a8eb65acbb4427aba95a36e', + width: 465, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=465&quality=45&auto=format&fit=max&dpr=2&s=a83822e43cb8b2e5138c64d9b703667b', + width: 930, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1900&quality=85&auto=format&fit=max&s=db58533a60c1446000a3d0d889e0512d', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=0e1930130117515b4a29bcf1a42764d2', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1300&quality=85&auto=format&fit=max&s=94eddf888c99942ba3c367663f45e81d', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=41125552ebbb4c30176627960e114cb3', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1140&quality=85&auto=format&fit=max&s=db421b10324267b4d5d830c58aef1488', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=b5b7cc0153063f623e13f39c4b81b5b8', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=980&quality=85&auto=format&fit=max&s=262ac49ecf7dec2d7e8d63c4ece04b44', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=2435f2779b29897db57274cc870d4e18', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=740&quality=85&auto=format&fit=max&s=4cfc8160b48f84b7b06edd1d0ff284f5', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=cc8c2f35c230cff765b140b55f3212ba', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=660&quality=85&auto=format&fit=max&s=aa8664c687af48eb4315aa7178de067e', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=d248dbebd46826663fcd653860f615f2', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=480&quality=85&auto=format&fit=max&s=b8dc516a0f27f23162be748fe35af966', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=1a4b5ea7e598d9ab2b399408a255a48d', + width: 960, + }, + ], + }, + ], + data: { + alt: "Anyday's glass storage containers in a refrigerator", + credit: 'Photograph: Courtesy of Anyday', + }, + }, + ], + main: '
Anyday\'s glass storage containers in a refrigerator
', + keyEvents: [], + blocks: [ + { + id: '667d2c5e8f08db81fc3259c7', + elements: [ + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

When I cook for my family, I always pack up leftover food with the best of intentions, and remnants of a weeknight meal generally get eaten within a day or two. Large spreads, however, are a different story.

', + elementId: '7afd6c4d-71bc-4567-a7a4-b515e013459c', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

In the past, I would stuff what was left from a large dinner or party into the biggest air-tight containers they required, then cram them all into my fridge, full-well believing my kids and I would live off of those leftovers for the better part of a week.

', + elementId: '5a402107-3623-4a29-9b14-4c7ea3eb5155', + }, + { + _type: 'model.dotcomrendering.pageElements.RichLinkBlockElement', + prefix: 'Related: ', + text: 'The five best induction cookware sets in the US, thoroughly tested and cooked in ', + elementId: 'fa05ecf9-92bd-478e-a0ea-02b5c389d6cf', + role: 'thumbnail', + url: 'https://www.theguardian.com/thefilter-us/2026/jan/09/best-induction-cookware', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

The reality, of course, is that my kids rarely want to eat back-to-back leftovers. And even I have to admit that previously prepared food is never quite as convenient as it seems when I’m staring down a new pile of dishes I dirtied for reheating and serving. I also cringe when I think about how much food went bad in the back of my refrigerator.

', + elementId: '200e7523-bae4-44ec-bcc3-38a9f865f17b', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

The good news is that there’s a better and far more sustainable way and it only took me twenty years of writing about food and four years of reviewing products (including storage and organization) for Bon Appétit and Epicurious to figure it out: putting leftovers into smaller glass containers. This method has rescued my leftover dishes, no matter how much I end up with.

', + elementId: 'eb2abb55-629e-44f7-8ddc-b7920370b1f7', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

At a glance

', + elementId: '325d25f3-bcb3-4558-8cfe-83ceff2b5e16', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '', + elementId: '45c3cc4b-3811-4939-97ba-74ee31dc5437', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Fcookanyday.com%2Fproducts%2F2-cup-round-dish-multipack&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: '$40 at Anyday', + elementId: 'b8999229-e414-4047-ad38-c16b9cfc6647', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '', + elementId: '73add693-9097-4741-87d4-75e12ca9eaf1', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Fcookanyday.com%2Fproducts%2F12-piece-round-dish-set&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: 'Now $180, originally $225 at Anyday ', + elementId: 'ca367d1b-7561-4c64-9af3-527981ebbd6e', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: 'a47555b1-fcb4-4fa4-a719-1e6df524aa72', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

Why store leftovers in smaller containers?

', + elementId: 'ffda62aa-3904-4775-916c-635f098e38be', + }, + { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + height: '2048', + width: '2048', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/2048.jpg', + }, + { + index: 1, + fields: { + isMaster: 'true', + height: '2048', + width: '2048', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg', + }, + { + index: 2, + fields: { + height: '2000', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/2000.jpg', + }, + { + index: 3, + fields: { + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/1000.jpg', + }, + { + index: 4, + fields: { + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/500.jpg', + }, + { + index: 5, + fields: { + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/140.jpg', + }, + ], + }, + elementId: 'e37cc07f-0684-4624-9b0a-04f60abc3bdc', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=85&auto=format&fit=max&s=67cdbe07a25c4184572c9b5060277af5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=aeb4e3d0eaf0956358e87d39985c7896', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=85&auto=format&fit=max&s=5c7cbc7abe1e94f4669c032537e46ac8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=574cb8e8645c2a99e1a15f356648e7a1', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=85&auto=format&fit=max&s=a13de41823792f4e2849abf64c727de2', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=16e99b731ac36d6345d43a06f6fa65ce', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=140&quality=85&auto=format&fit=max&s=ccb27c3c68f54b856d074acbe0999bf0', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=ef2be56ea985d1dce76924de4ffff1ef', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=120&quality=85&auto=format&fit=max&s=f14795810724f2a758ba34622fe1566a', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=5dac311ffe87f890a54e6a1b7bbcb7ce', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=380&quality=85&auto=format&fit=max&s=743141aead31808fff684973bfaef991', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=c0c992c2738b6033dd52a10da7c40efc', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=300&quality=85&auto=format&fit=max&s=ef07f790c94ce9ff1d6385b988b0278a', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=3650878c59b8c0812ecc895290feab5c', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=85&auto=format&fit=max&s=67cdbe07a25c4184572c9b5060277af5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=aeb4e3d0eaf0956358e87d39985c7896', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=85&auto=format&fit=max&s=5c7cbc7abe1e94f4669c032537e46ac8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=574cb8e8645c2a99e1a15f356648e7a1', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=85&auto=format&fit=max&s=a13de41823792f4e2849abf64c727de2', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=16e99b731ac36d6345d43a06f6fa65ce', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=860&quality=85&auto=format&fit=max&s=db1af69098398e29e80cfbdd0d273f74', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=112c4887d4dcedc6b524d4ed26bf8846', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=780&quality=85&auto=format&fit=max&s=fc4e5c4dd2a1b38460888f44125ff50d', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=892e5a2773db3d8f5dcc8b4114a10eb8', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=85&auto=format&fit=max&s=67cdbe07a25c4184572c9b5060277af5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=aeb4e3d0eaf0956358e87d39985c7896', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=85&auto=format&fit=max&s=5c7cbc7abe1e94f4669c032537e46ac8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=574cb8e8645c2a99e1a15f356648e7a1', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=85&auto=format&fit=max&s=a13de41823792f4e2849abf64c727de2', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=16e99b731ac36d6345d43a06f6fa65ce', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=85&auto=format&fit=max&s=67cdbe07a25c4184572c9b5060277af5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=aeb4e3d0eaf0956358e87d39985c7896', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=85&auto=format&fit=max&s=5c7cbc7abe1e94f4669c032537e46ac8', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=574cb8e8645c2a99e1a15f356648e7a1', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=85&auto=format&fit=max&s=a13de41823792f4e2849abf64c727de2', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=16e99b731ac36d6345d43a06f6fa65ce', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=1900&quality=85&auto=format&fit=max&s=b27224cbe49ad2da23ee114b9d0fabe0', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=8346f8268ac27b2d55568f155dfc4202', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=1300&quality=85&auto=format&fit=max&s=9297d80c8bd52dc8600619b044c6c833', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=d5ac7c0b9078c3c1a5dbd392d026e1b5', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=1140&quality=85&auto=format&fit=max&s=40a81a5b6bcf70364b7c00a3c175e982', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=ca4c56686d2f7d0363d9e004a977a658', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=980&quality=85&auto=format&fit=max&s=b2467be4a88ce4598b7b7bea68ec0ad7', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=2eec08ca27570fd2dff54b52876e5ca4', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=740&quality=85&auto=format&fit=max&s=21a1e08fb3fea51abb39042bcbf89fb4', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=b58be0be5e18fc9196735619e63a87be', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=660&quality=85&auto=format&fit=max&s=d556e5a2f8229b6b257fc400ae9a5cf5', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=1ef839d8768b1bfd2600e766b8ba9e17', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=480&quality=85&auto=format&fit=max&s=bfc3b4aa47f64ce8eb0f605aa314aa98', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/fc76fe0883c776faa45b6473e0c21403ba04afa3/0_0_2048_2048/master/2048.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=73dd8dadf3e3f3b7a8b7b9414bdb7564', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Stasher Stretch Lids Large 3-Pack', + credit: 'Photograph: Courtesy of Stasher', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

While food storage containers come in all shapes and sizes, I find a stackable two-cup size the best format for a few reasons. For starters, it’s easier to make room in the fridge for a handful of smaller containers than a big baking dish or bowl. Dividing loads of leftovers between smaller containers has also helped me cut way back on food waste, because whatever doesn’t get consumed within a day or two can go straight into the freezer.

', + elementId: 'fb8820c9-5e3d-4076-a859-fc83bfce1b10', + }, + { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'supporting', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '1763', + width: '1763', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/1763.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '1763', + width: '1763', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/1000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/500.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/140.jpg', + }, + ], + }, + elementId: '92620a45-26a8-4f5e-859d-cb3c2ce9c146', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=85&auto=format&fit=max&s=08f098667279c25eedb6c5568118f782', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=362e640ff00087bff212e82d95df3e50', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=85&auto=format&fit=max&s=ea65ea75059f6a28cd3e5937c75df05e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=531f5a1eec8744289f1e07430228bbab', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=85&auto=format&fit=max&s=d29660cd8f76ff90953eafe6319fc9eb', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=58ac4952dcaf6b1fd428ecc674d5def9', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=140&quality=85&auto=format&fit=max&s=4778def0e26247cc008e85c32c72e543', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=b01cc4aba877839f4737398233c8521d', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=120&quality=85&auto=format&fit=max&s=2f53bd3aabb152a334c03de126e9ff9d', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=cc85f311bf95f8d8db0d064ca43f1ae9', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=380&quality=85&auto=format&fit=max&s=31c856d3519efc3d8e38cc4489ff763f', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=9575cdd153ac9bf8a9cd245cb6ac0916', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=300&quality=85&auto=format&fit=max&s=f8c03071d43d2ec941ca163d4d4fa165', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=991203159f9d5811a0737aa66d3472de', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=85&auto=format&fit=max&s=08f098667279c25eedb6c5568118f782', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=362e640ff00087bff212e82d95df3e50', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=85&auto=format&fit=max&s=ea65ea75059f6a28cd3e5937c75df05e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=531f5a1eec8744289f1e07430228bbab', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=85&auto=format&fit=max&s=d29660cd8f76ff90953eafe6319fc9eb', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=58ac4952dcaf6b1fd428ecc674d5def9', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=860&quality=85&auto=format&fit=max&s=86fcce284b7b6e6d489702ab3b0c2463', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=3b8caeee596da43de9881a61bd207d4d', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=780&quality=85&auto=format&fit=max&s=689ea47f5c6d72b54d320d626cd88765', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=7de06dd4baca7e851bbe114b876c8b17', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=85&auto=format&fit=max&s=08f098667279c25eedb6c5568118f782', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=362e640ff00087bff212e82d95df3e50', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=85&auto=format&fit=max&s=ea65ea75059f6a28cd3e5937c75df05e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=531f5a1eec8744289f1e07430228bbab', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=85&auto=format&fit=max&s=d29660cd8f76ff90953eafe6319fc9eb', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=58ac4952dcaf6b1fd428ecc674d5def9', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=85&auto=format&fit=max&s=08f098667279c25eedb6c5568118f782', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=362e640ff00087bff212e82d95df3e50', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=85&auto=format&fit=max&s=ea65ea75059f6a28cd3e5937c75df05e', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=531f5a1eec8744289f1e07430228bbab', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=85&auto=format&fit=max&s=d29660cd8f76ff90953eafe6319fc9eb', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=58ac4952dcaf6b1fd428ecc674d5def9', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=1900&quality=85&auto=format&fit=max&s=854a1db1cfeb3cf0b26986b2ebe3efcf', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=8322007ec5cf1edb6255cf177af2d51f', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=1300&quality=85&auto=format&fit=max&s=64f12c47b7d296670d7b7f99b174df0e', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=e889223d677f577d18f135ab47296879', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=1140&quality=85&auto=format&fit=max&s=21daaa20599a8a2303826532aaf035b2', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=1dcec2f9184db03ef8b9d6abaa988d02', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=980&quality=85&auto=format&fit=max&s=a0c0bf2423dd6d4a8f98f2d8d35d0a9a', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=b7ae67e4e11dba6ab1b753ea82e72ea8', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=740&quality=85&auto=format&fit=max&s=efbb403671fd3f7d3ccf0e6934de7fbe', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=17daefc01f3a594f86436a5490843826', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=660&quality=85&auto=format&fit=max&s=8076c62eea721fbb29d7fca47782bd38', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=b8a1cc1663a554bae79e54f98ab9f43f', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=480&quality=85&auto=format&fit=max&s=b4abbfc2c78f099ccf649b4de59e6749', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/34b3dd05ff9b06c4cd81d1da7f93266234aa582c/131_218_1763_1763/master/1763.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=593967d94c0b6f44ca5fc889418e9cd5', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Stasher Stretch Lids Large 3-Pack', + credit: 'Photograph: Courtesy of Stasher', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

An added bonus? Fewer dirty dishes, because instead of hauling a loosely-covered casserole pan from the fridge and transferring some of its contents to a heat-safe bowl, I can pull a perfectly-portioned serving and pop it right into the microwave or oven. (More on the best microwave-safe and oven-safe storage containers below.) This works whether I’m reheating a side of asparagus for the family or a one-dish dinner of meat, potatoes and broccoli for myself. (And if divvying up leftovers must wait until the morning, you can still create an airtight seal on your serving dishes with a stretchy silicone lid.)

', + elementId: 'ef559d9b-5856-4400-a7aa-cb4e91c12ab7', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Stasher Stretch Lids Large 3-Pack

', + elementId: '6f53354c-08e7-4399-8752-7e7a829db9c0', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Famazon.com%2Fdp%2FB0DXNJYQJ5&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: '$13.59 at Amazon', + elementId: '64be53d1-de18-4f2c-b133-5e7f2471bc75', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Fwww.stasherbag.com%2Fcollections%2Flids%2Fproducts%2Fstretch-lids-large-3-pack&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: '$19.99 at Stasher', + elementId: 'edb48deb-7ee8-4f83-999e-5f178ed6a174', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '8eb27d6d-0312-43ff-b26e-282805dbb29a', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

What’s the best and safest material for food storage containers?

', + elementId: '73034151-7b8c-4ddb-b87c-436b812701cf', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Thick, heat-tolerant glass is, by far, the best and most versatile material for storing food because it’s safe to use in the fridge, freezer, microwave, oven and dishwasher.

', + elementId: '4a93f26c-ef0a-48ed-b534-f3f005f6e37f', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Unlike plastic, you never have to worry about tomato sauce stains or leeching chemicals, and as long as you don’t drop it (or thermal shock it with a sudden and drastic change of temperature) it will theoretically last forever. The only problem is that most – including the Pyrex system I used for more than a decade – will outlive their flimsy plastic lids.

', + elementId: 'ba5fb4b5-7b72-4aa8-b588-190fe71814ca', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: 'd3f9da9d-1c7a-46ac-bec3-701755e12866', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

What’s the best food storage container for leftovers?

', + elementId: 'bdb893c0-e690-4ac7-96aa-8d7232897589', + }, + { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + height: '1425', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/2000.jpg', + }, + { + index: 1, + fields: { + height: '1425', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/2000.jpg', + }, + { + index: 2, + fields: { + isMaster: 'true', + height: '1425', + width: '2000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg', + }, + { + index: 3, + fields: { + height: '713', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/1000.jpg', + }, + { + index: 4, + fields: { + height: '356', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/500.jpg', + }, + { + index: 5, + fields: { + height: '100', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/140.jpg', + }, + ], + }, + elementId: 'c2978334-6c61-4230-9684-f912aac4e8a9', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=85&auto=format&fit=max&s=6df1a049fdd6d2e6a076a5d39b1ea154', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=84c92d7005a23ab47826ff7db5b880d5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=85&auto=format&fit=max&s=fb6809af72687303ae94c828568ecbd7', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=aef7574a117c3be84907d281cd6a840e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=85&auto=format&fit=max&s=2b182464d9acf172d15a3bf6076bce31', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=fb11dd5321063fb0a1ae51fa4ea1e649', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=140&quality=85&auto=format&fit=max&s=95db7456d0bc278250dd3df36e8058ae', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=b859495f39d2e73369cb484ecc40ebca', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=120&quality=85&auto=format&fit=max&s=8030213bf4b6cb343aa86f1b43fccd06', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=e6b2bcbd703d4d1ca3394006428c6076', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=380&quality=85&auto=format&fit=max&s=739976350d9de92d64d3728a6efa5cfc', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=9d4212ac34dc79ac5cf08d3416403e32', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=300&quality=85&auto=format&fit=max&s=24b0b86f61f516dfeaa74942386fa746', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=544aaf5dbc6a22f9a7cb159d4fbdce41', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=85&auto=format&fit=max&s=6df1a049fdd6d2e6a076a5d39b1ea154', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=84c92d7005a23ab47826ff7db5b880d5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=85&auto=format&fit=max&s=fb6809af72687303ae94c828568ecbd7', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=aef7574a117c3be84907d281cd6a840e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=85&auto=format&fit=max&s=2b182464d9acf172d15a3bf6076bce31', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=fb11dd5321063fb0a1ae51fa4ea1e649', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=860&quality=85&auto=format&fit=max&s=3c4ee713044fd6701a3f65de89eb0ef1', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=b865307d8ea2a8645ec667cb418f2cde', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=780&quality=85&auto=format&fit=max&s=6d0b1ec230e5d74a58d3ec0651c09a67', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=d5c7489ac8db0d2cc622f08cf5cb2c51', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=85&auto=format&fit=max&s=6df1a049fdd6d2e6a076a5d39b1ea154', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=84c92d7005a23ab47826ff7db5b880d5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=85&auto=format&fit=max&s=fb6809af72687303ae94c828568ecbd7', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=aef7574a117c3be84907d281cd6a840e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=85&auto=format&fit=max&s=2b182464d9acf172d15a3bf6076bce31', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=fb11dd5321063fb0a1ae51fa4ea1e649', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=85&auto=format&fit=max&s=6df1a049fdd6d2e6a076a5d39b1ea154', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=84c92d7005a23ab47826ff7db5b880d5', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=85&auto=format&fit=max&s=fb6809af72687303ae94c828568ecbd7', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=aef7574a117c3be84907d281cd6a840e', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=85&auto=format&fit=max&s=2b182464d9acf172d15a3bf6076bce31', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=fb11dd5321063fb0a1ae51fa4ea1e649', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=1900&quality=85&auto=format&fit=max&s=917a0acf48d1d244a624d67055ac3a3b', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=b94aa93de8f7ffc6d1d048ba296c06cc', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=1300&quality=85&auto=format&fit=max&s=23dba5e969337e5ab0d689a8ad764010', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=9448754ba039e5ec7167906d9bf1c9a4', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=1140&quality=85&auto=format&fit=max&s=26ba050704196d37475fb0f2c5408d9b', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=1bce9018844a4a38403132d441c0361b', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=980&quality=85&auto=format&fit=max&s=f5695e002a3e0a5e2102a36ed954ac7b', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=a9e1a95e24799b31cca250a969a8df4c', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=740&quality=85&auto=format&fit=max&s=431fce3c801c2a93ae200f80761cabf2', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=050fa2bcea7cefa7ef36becbf420394a', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=660&quality=85&auto=format&fit=max&s=4b00e0e9eebce0289b7a2a69144e6774', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=dc7f7b487597dc8176b32402777afb3d', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=480&quality=85&auto=format&fit=max&s=e09f7aaed91e8ac7540cd2db2892a063', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/19e1aa42251360d15f8bafe044e2dbea120d2ad8/0_556_2000_1425/master/2000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=d192c06b93b0640b503b9df126633914', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Anyday 2-Cup Glass Round Dish Multipack', + credit: 'Photograph: Courtesy of Williams Sonoma', + }, + }, + { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'supporting', + media: { + allImages: [ + { + index: 0, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/1000.jpg', + }, + { + index: 1, + fields: { + aspectRatio: '1:1', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/1000.jpg', + }, + { + index: 2, + fields: { + aspectRatio: '1:1', + isMaster: 'true', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg', + }, + { + index: 3, + fields: { + aspectRatio: '1:1', + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/500.jpg', + }, + { + index: 4, + fields: { + aspectRatio: '1:1', + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/140.jpg', + }, + ], + }, + elementId: '8d4f1805-eaae-4474-88f5-afde5082dfe6', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=386a4a2c2d846b1241666d785c73efe5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=2147a4d651bf1fc6cd79884b147d186f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=4cf287ff4a1ef389241022dff8731135', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=af23500651bef67b999caf94ccbe4a70', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=524ca7800fd8054d6276bdfac334f707', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=43249eedf6baa7b84f50a9589a177f38', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=140&quality=85&auto=format&fit=max&s=6cc9a8d8380b07c6ab0a5917f6b9ea0c', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=0ff9f60cab3fa2c9c9394f66ac53a867', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=120&quality=85&auto=format&fit=max&s=c0097f1fb22ce34a9b07ef61d8b32986', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=0d1008c4333b43c6427ea0e143cb0a94', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=380&quality=85&auto=format&fit=max&s=9f37149d6f0d61c2d14de5edea6f8dfa', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=e93c850757599b5f303d13dece083d6f', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=300&quality=85&auto=format&fit=max&s=cfe9bdd5530327e762e68ed4543f9d25', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=ad8cb3dc7eac79973ac4dfad666c4ab8', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=386a4a2c2d846b1241666d785c73efe5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=2147a4d651bf1fc6cd79884b147d186f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=4cf287ff4a1ef389241022dff8731135', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=af23500651bef67b999caf94ccbe4a70', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=524ca7800fd8054d6276bdfac334f707', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=43249eedf6baa7b84f50a9589a177f38', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=860&quality=85&auto=format&fit=max&s=ba5cb1a91d270571a5aaa0dfd1fe5669', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=f343e5387d63f7cece06cde1ced0c8d3', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=780&quality=85&auto=format&fit=max&s=956246cd3b2e2c56332f3089e3190917', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=f244dbbd3a1be958da4398a5d4253bca', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=386a4a2c2d846b1241666d785c73efe5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=2147a4d651bf1fc6cd79884b147d186f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=4cf287ff4a1ef389241022dff8731135', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=af23500651bef67b999caf94ccbe4a70', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=524ca7800fd8054d6276bdfac334f707', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=43249eedf6baa7b84f50a9589a177f38', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=386a4a2c2d846b1241666d785c73efe5', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=2147a4d651bf1fc6cd79884b147d186f', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=4cf287ff4a1ef389241022dff8731135', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=af23500651bef67b999caf94ccbe4a70', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=524ca7800fd8054d6276bdfac334f707', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=43249eedf6baa7b84f50a9589a177f38', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=1900&quality=85&auto=format&fit=max&s=8bf5f5764cb6baa75356ee8475d2f0fc', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=ad2531a6963a80339ef0979579432231', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=1300&quality=85&auto=format&fit=max&s=c6869a730a8c3bc4135466fc56039fa3', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=21819c5581e323cfa2011a06a3f285d6', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=1140&quality=85&auto=format&fit=max&s=eb0ae9d604c3c01709c2089e4f8594f0', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=f6443b4b8977e00c852030f2a71ee138', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=980&quality=85&auto=format&fit=max&s=5834e398c48744fd47ea9d1bcdac2150', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=fee1da5340dc7595183146305e108e55', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=740&quality=85&auto=format&fit=max&s=e1be7c12a25de5e7ab193074a636ef0f', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=43d58a1c383a93d7a1a3a561f3da2b59', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=660&quality=85&auto=format&fit=max&s=465cce83d5e7f43fa0d7f29f0f4e6925', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=031132310a7474829f2c631465736b39', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=480&quality=85&auto=format&fit=max&s=f4cfaec89c98780e58ae98270115388b', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/908b4903e06f83f05f5316e99a921d900980dc67/0_0_1000_1000/master/1000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=f954cb2312d40693e6a99e45a3105983', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Anyday 2-Cup Glass Round Dish Multipack', + credit: 'Photograph: Courtesy of Williams Sonoma', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Earlier last year, I discovered Anyday’s glass food storage containers, and though they seemed a bit gimmicky at first, I quickly realized they’re a game changer for storing and reheating leftovers. After using them for just a few months, I noticed my family was wasting less food – and I was doing fewer dishes.

', + elementId: '6be04830-4deb-4aa7-a5c0-caaa9c759b55', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Anyday 2-Cup Glass Round Dish Multipack

', + elementId: '7f45ce02-8c99-4384-92a0-f0a7759e5c63', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Fcookanyday.com%2Fproducts%2F2-cup-round-dish-multipack&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: '$40 at Anyday', + elementId: '4284c683-57b5-4ebf-9194-6055faca3fec', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Famazon.com%2Fdp%2FB0F4B5NNJR&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: '$39.99 at Amazon', + elementId: '4cf9f21e-38ed-4f81-8165-0dc4ffbcf48e', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

Anyday’s containers are made of thick, durable glass that you can put in the fridge, freezer, microwave, oven and even the air fryer up to 500F. Then you can pop all of the parts into the dishwasher when you’re done. All of the containers are plastic-free and non-toxic.

', + elementId: '2ec0f5a3-b061-4ba3-95ae-91641c740305', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

But, unlike other popular glass food storage containers, these are marketed as “cookware and food storage”. The glass lids are cleverly designed for cooking and reheating, with a heat-safe silicone knob that doubles as a vent; just pull it up before microwaving or baking for perfectly-steamed food. I love it for reheating rice, soup and even barbecue sandwiches. A silicone gasket around the rim creates an air-tight seal for the fridge and freezer, and it’s leak-proof enough I feel safe packing soup in it for my five-year-old’s school lunch. (So far, so good!)

', + elementId: 'b9909c0d-40f1-4198-a7c5-4b22af3b14b4', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: 'd8d3f94f-7921-4fec-b907-9903eb19795c', + }, + { + _type: 'model.dotcomrendering.pageElements.SubheadingBlockElement', + html: '

Can you really cook in Anyday’s food storage containers?

', + elementId: '78e00f4e-dfbc-4941-8d92-c6784c04c64a', + }, + { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'inline', + media: { + allImages: [ + { + index: 0, + fields: { + height: '819', + width: '1040', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/1040.jpg', + }, + { + index: 1, + fields: { + isMaster: 'true', + height: '819', + width: '1040', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg', + }, + { + index: 2, + fields: { + height: '788', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/1000.jpg', + }, + { + index: 3, + fields: { + height: '394', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/500.jpg', + }, + { + index: 4, + fields: { + height: '110', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/140.jpg', + }, + ], + }, + elementId: '50582aeb-c4ef-4831-b467-64d889657d20', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=85&auto=format&fit=max&s=76b40201f85ae2d8845885ad5f5dcc34', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=cf3ea02013a7fdd9ffc622ab4b451428', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=85&auto=format&fit=max&s=bda2abae151effcc526fd08c4b1b1407', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=b369e5f5b1fb2d28a8a177b3d5c34b84', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=85&auto=format&fit=max&s=95510471dafa3a37c9e031bf8a7812ba', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=9c6978a6d21b61ea4b4be406132fefa6', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=140&quality=85&auto=format&fit=max&s=0b3ca4c38ef659c36e4131f574fb5d3d', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=2b924cc22e8a1f43e01968a4efa06658', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=120&quality=85&auto=format&fit=max&s=33c1e4859271bc90db8d90af39b87b15', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=244c46c5bfbb6c0533b62b2e6bc49ade', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=380&quality=85&auto=format&fit=max&s=345192866b3deddd226cc3081f83667b', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=4779f1747aa63ca91e773cf194e0ea07', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=300&quality=85&auto=format&fit=max&s=0bc060659241ed7090185b193711fce4', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=1d932931d28aa5bdc80ef38640d2a319', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=85&auto=format&fit=max&s=76b40201f85ae2d8845885ad5f5dcc34', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=cf3ea02013a7fdd9ffc622ab4b451428', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=85&auto=format&fit=max&s=bda2abae151effcc526fd08c4b1b1407', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=b369e5f5b1fb2d28a8a177b3d5c34b84', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=85&auto=format&fit=max&s=95510471dafa3a37c9e031bf8a7812ba', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=9c6978a6d21b61ea4b4be406132fefa6', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=860&quality=85&auto=format&fit=max&s=f43ff454635137bc0a4b5e94d9706427', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=c4db7dbf0c67c86d42978132ce95ca90', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=780&quality=85&auto=format&fit=max&s=f53772ad4710d281393beff2d81ed4aa', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=49b632a4cdbfaebccd7effb14a54f9d3', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=85&auto=format&fit=max&s=76b40201f85ae2d8845885ad5f5dcc34', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=cf3ea02013a7fdd9ffc622ab4b451428', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=85&auto=format&fit=max&s=bda2abae151effcc526fd08c4b1b1407', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=b369e5f5b1fb2d28a8a177b3d5c34b84', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=85&auto=format&fit=max&s=95510471dafa3a37c9e031bf8a7812ba', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=9c6978a6d21b61ea4b4be406132fefa6', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=85&auto=format&fit=max&s=76b40201f85ae2d8845885ad5f5dcc34', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=cf3ea02013a7fdd9ffc622ab4b451428', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=85&auto=format&fit=max&s=bda2abae151effcc526fd08c4b1b1407', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=b369e5f5b1fb2d28a8a177b3d5c34b84', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=85&auto=format&fit=max&s=95510471dafa3a37c9e031bf8a7812ba', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=9c6978a6d21b61ea4b4be406132fefa6', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=1900&quality=85&auto=format&fit=max&s=a6ce1320eccb4da2c4a13823abad4740', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=79886e800c56341b3679672f086e9d9d', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=1300&quality=85&auto=format&fit=max&s=c040ea432846f8c77bf230c237ee1186', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=300e41da1f63848a73d14da4c0f0690b', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=1140&quality=85&auto=format&fit=max&s=2c806034cd387d30eeb7f76aa1cd8659', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=32bf8e656b7ece03b338e5594b5a259a', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=980&quality=85&auto=format&fit=max&s=c83879961af668fe47e0aeab21251880', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=d6d231062800b7f9f7ecc2aa320598ce', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=740&quality=85&auto=format&fit=max&s=ec1dc94cde1109f33a98e6d0dcbbda2e', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=9cc3c4e7127aeae2849afcc39312f2f8', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=660&quality=85&auto=format&fit=max&s=c392b161f43e2902f193f3272d6bd584', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=b37f537ab6577e17cd6d96e6973ecf83', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=480&quality=85&auto=format&fit=max&s=9e6602f1b11bd3d52f9d1acc67ffba32', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/856abaa2950f5081be3a09ad2dbb5e793bd60757/0_0_1040_819/master/1040.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=a0f59212921f513caaf6eb6fb327136f', + width: 960, + }, + ], + }, + ], + data: { + alt: 'Anyday 10-Cup Glass Square Shallow Dish', + credit: 'Photograph: Courtesy of Anyday', + }, + }, + { + displayCredit: true, + _type: 'model.dotcomrendering.pageElements.ImageBlockElement', + role: 'supporting', + media: { + allImages: [ + { + index: 0, + fields: { + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/1000.jpg', + }, + { + index: 1, + fields: { + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/1000.jpg', + }, + { + index: 2, + fields: { + isMaster: 'true', + height: '1000', + width: '1000', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg', + }, + { + index: 3, + fields: { + height: '500', + width: '500', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/500.jpg', + }, + { + index: 4, + fields: { + height: '140', + width: '140', + }, + mediaType: 'Image', + mimeType: 'image/jpeg', + url: 'https://media.guim.co.uk/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/140.jpg', + }, + ], + }, + elementId: '49891b7e-3399-42fb-a953-73ee3e3fc0cd', + imageSources: [ + { + weighting: 'inline', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=af92bea362bde44b42831dd731c8d406', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f98f019183102401826baff138e4180a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=d1bae3f383f7b12fa9d5485fd306fff2', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=d64b5a88d552024d031141f29d1717a5', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=64ace11481767394b79c72631cd68c9e', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=48ec08dead03d76db86e14cd6f74ad31', + width: 890, + }, + ], + }, + { + weighting: 'thumbnail', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=140&quality=85&auto=format&fit=max&s=dbc79a9bc2fd595113130c93c3ec44bc', + width: 140, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=140&quality=45&auto=format&fit=max&dpr=2&s=38d5ca3bc9a686a3246ca04030f8231c', + width: 280, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=120&quality=85&auto=format&fit=max&s=802f75bf9a4cef62cc1b8eae3b0b64d0', + width: 120, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=120&quality=45&auto=format&fit=max&dpr=2&s=844bdb36787e28e0ef043cfa8b2ba591', + width: 240, + }, + ], + }, + { + weighting: 'supporting', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=380&quality=85&auto=format&fit=max&s=8e4b8ac4c39b4de15644b9d9088355a4', + width: 380, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=380&quality=45&auto=format&fit=max&dpr=2&s=10eaff147fd92f9f4f40bf33e3b1588f', + width: 760, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=300&quality=85&auto=format&fit=max&s=37e4415984e2ecbdc0836e0fde75e178', + width: 300, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=300&quality=45&auto=format&fit=max&dpr=2&s=b663a6c4e206ce6bd9ca9fb944dfa93d', + width: 600, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=af92bea362bde44b42831dd731c8d406', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f98f019183102401826baff138e4180a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=d1bae3f383f7b12fa9d5485fd306fff2', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=d64b5a88d552024d031141f29d1717a5', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=64ace11481767394b79c72631cd68c9e', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=48ec08dead03d76db86e14cd6f74ad31', + width: 890, + }, + ], + }, + { + weighting: 'showcase', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=860&quality=85&auto=format&fit=max&s=afe007fc3f756094c86715a94094761c', + width: 860, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=860&quality=45&auto=format&fit=max&dpr=2&s=4601267d1827f8fa346ea49b282622c0', + width: 1720, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=780&quality=85&auto=format&fit=max&s=4f13ca5bce543e69713d5333f33c2fed', + width: 780, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=780&quality=45&auto=format&fit=max&dpr=2&s=74c8d99ef99bb610320f7c97d2eb9931', + width: 1560, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=af92bea362bde44b42831dd731c8d406', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f98f019183102401826baff138e4180a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=d1bae3f383f7b12fa9d5485fd306fff2', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=d64b5a88d552024d031141f29d1717a5', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=64ace11481767394b79c72631cd68c9e', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=48ec08dead03d76db86e14cd6f74ad31', + width: 890, + }, + ], + }, + { + weighting: 'halfwidth', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=85&auto=format&fit=max&s=af92bea362bde44b42831dd731c8d406', + width: 620, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=620&quality=45&auto=format&fit=max&dpr=2&s=f98f019183102401826baff138e4180a', + width: 1240, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=85&auto=format&fit=max&s=d1bae3f383f7b12fa9d5485fd306fff2', + width: 605, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=605&quality=45&auto=format&fit=max&dpr=2&s=d64b5a88d552024d031141f29d1717a5', + width: 1210, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=85&auto=format&fit=max&s=64ace11481767394b79c72631cd68c9e', + width: 445, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=445&quality=45&auto=format&fit=max&dpr=2&s=48ec08dead03d76db86e14cd6f74ad31', + width: 890, + }, + ], + }, + { + weighting: 'immersive', + srcSet: [ + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=1900&quality=85&auto=format&fit=max&s=6a3f3167d597a7f06d40f4b5f28efdc3', + width: 1900, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=1900&quality=45&auto=format&fit=max&dpr=2&s=7040fe499a03d96c523a2128107e5881', + width: 3800, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=1300&quality=85&auto=format&fit=max&s=43fc918c0a6f661f0d450048d4b10cae', + width: 1300, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=1300&quality=45&auto=format&fit=max&dpr=2&s=0da3f82a50f2ad4e483e525a44a49bed', + width: 2600, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=1140&quality=85&auto=format&fit=max&s=51a052c290613b0aab2ade094b2f2dea', + width: 1140, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=1140&quality=45&auto=format&fit=max&dpr=2&s=c04b38192c9be2275cbcbacede378a9d', + width: 2280, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=980&quality=85&auto=format&fit=max&s=b1d81a3aee0e325a7a1a183808405290', + width: 980, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=980&quality=45&auto=format&fit=max&dpr=2&s=7b19473a1d9c5684914af172e157a498', + width: 1960, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=740&quality=85&auto=format&fit=max&s=ce1e62a55b851d5dbf0528393584d585', + width: 740, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=740&quality=45&auto=format&fit=max&dpr=2&s=fd29b8d11d6dbdbba7d2776536d38e18', + width: 1480, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=660&quality=85&auto=format&fit=max&s=d630a0a6df9f0599f945ae139fe0cc54', + width: 660, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=660&quality=45&auto=format&fit=max&dpr=2&s=8feebc9d7b4c9335304296b7b2c1ea95', + width: 1320, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=480&quality=85&auto=format&fit=max&s=2eefda977d9e13cf5106d13894add921', + width: 480, + }, + { + src: 'https://i.guim.co.uk/img/media/3dfae38b75aa6c74b1a15fc5f6817d8aa40775f8/0_0_1000_1000/master/1000.jpg?width=480&quality=45&auto=format&fit=max&dpr=2&s=8e44a5bcdb6827d628b11d1d0599ac43', + width: 960, + }, + ], + }, + ], + data: { + alt: '10-Cup Glass Square Shallow Dish', + credit: 'Photograph: Courtesy of Anyday', + }, + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

While I mostly use Anyday’s containers for storing and reheating leftovers, they’re great for quick steam cooking (using the clip-on strainer) as well as more efficient meal prep. In addition to being a plastic-free alternative for anything that’s supposed to be microwaved in the bag (think: shelf-stable rice and frozen foods), you can use them to cook fresh veggies, potatoes, rice, fish and more from start to finish. (Yes, you can cook fish in the microwave!) Lids on some of the larger pieces, like the 10-cup square shallow dish, are reinforced with a microwave-safe stainless steel rim for extra durability.

', + elementId: '5eeaa447-b2ac-4697-870b-74d498f30e44', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

10-Cup Glass Square Shallow Dish

', + elementId: 'fc0496ea-85c8-40b5-b03d-165c22628d71', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Fcookanyday.com%2Fcollections%2Fshallow-dishes%2Fproducts%2F10-cup-square-shallow-dish&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: '$55 at Anyday', + elementId: '3dd8a304-d12c-4015-9bc4-651a798cff29', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.LinkBlockElement', + priority: 'Primary', + url: 'https://go.skimresources.com/?id=114047X1771840&url=https%3A%2F%2Famazon.com%2Fdp%2FB0F3P81XVY&sref=https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers.json?dcr', + label: '$54.99 at Amazon', + elementId: '09cdf5bf-8e5c-44df-83d7-09e6447f1092', + linkType: 'ProductButton', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

I certainly wouldn’t want to microwave all of my meals, but the glass containers can be used in the oven to bake and roast anything you’d make in a traditional glass baking dish. So minimalists and cooks short on kitchen space, you can rejoice: an Anyday food storage set could be a great way to consolidate your food storage and cookware for your future meals.

', + elementId: '0599360f-bdc5-4d0d-bdb7-623de1de8711', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: 'aa79c7fc-bcad-45b5-b1bf-ada22a60dc27', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

The Filter US is your guide to buying fewer, better things - including those with less plastic. More kitchen stories you might enjoy:

', + elementId: '79629acb-ec6a-4bde-8f79-11ab795f4c81', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '', + elementId: '7b7a318e-94d9-4854-9cf6-db334110b240', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '

***

', + elementId: '6dbcb2c2-d22c-472a-8b44-d065423fd287', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '
    \n
  • \n

    Emily Farris has been writing about food and lifestyle for nearly 20 years. She was formerly senior commerce writer at Bon Appétit and Epicurious, where she rigorously tested cooking gear. She is the author of the personal essay collection I’ll Just Be Five More Minutes: And Other Tales from My ADHD Brain (Hachette Books, 2024).

  • \n
', + elementId: 'f9fd55f5-b2ce-4a1e-a608-49086e5b2fdd', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '
    \n
  • \n

    This story was updated in March 2026 for accurate prices and product information.

  • \n
', + elementId: 'fb920ca1-3d76-4447-b02b-ed8428e126c4', + }, + { + _type: 'model.dotcomrendering.pageElements.TextBlockElement', + html: '
    \n
  • \n

    The Filter US recommends safe, sustainable and plastic-free products that are better for you and the environment. Have a recommendation – or disagree with our review? Email thefilter.us@theguardian.com

  • \n
', + elementId: '33da6662-a371-4b35-9863-debeba7ca4ea', + }, + ], + attributes: { + pinned: false, + keyEvent: false, + summary: false, + }, + blockCreatedOn: 1766675742000, + blockCreatedOnDisplay: '15.15 GMT', + blockLastUpdated: 1777651332000, + blockLastUpdatedDisplay: '17.02 BST', + blockFirstPublished: 1771531225000, + blockFirstPublishedDisplay: '20.00 GMT', + blockFirstPublishedDisplayNoTimezone: '20.00', + contributors: [], + primaryDateLine: 'Fri 3 Apr 2026 13.45 BST', + secondaryDateLine: 'First published on Thu 25 Dec 2025 15.15 GMT', + }, + ], + author: { + byline: 'Emily Farris', + }, + byline: 'Emily Farris', + webPublicationDate: '2026-04-03T12:45:31.000Z', + webPublicationDateDeprecated: '2026-04-03T12:45:31.000Z', + webPublicationDateDisplay: 'Fri 3 Apr 2026 13.45 BST', + webPublicationSecondaryDateDisplay: + 'First published on Thu 25 Dec 2025 15.15 GMT', + editionLongForm: 'UK edition', + editionId: 'UK', + pageId: 'thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + canonicalUrl: + 'https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + format: { + design: 'ReviewDesign', + theme: 'LifestylePillar', + display: 'StandardDisplay', + }, + designType: 'Review', + tags: [ + { + id: 'thefilter-us/series/thefilter-us', + type: 'Series', + title: 'The Filter US', + }, + { + id: 'lifeandstyle/lifeandstyle', + type: 'Keyword', + title: 'Life and style', + }, + { + id: 'campaign/email/the-filter-us', + type: 'Campaign', + title: 'The Filter US (newsletter signup)', + }, + { + id: 'food/series/kitchen-aide', + type: 'Series', + title: 'Kitchen aide', + }, + { + id: 'thefilter-us/series/plastic-free-life', + type: 'Series', + title: 'Plastic-free life', + }, + { + id: 'tone/features', + type: 'Tone', + title: 'Features', + }, + { + id: 'type/article', + type: 'Type', + title: 'Article', + }, + { + id: 'tone/best-ofs', + type: 'Tone', + title: 'Best of', + }, + { + id: 'tone/reviews', + type: 'Tone', + title: 'Reviews', + }, + { + id: 'profile/emily-farris', + type: 'Contributor', + title: 'Emily Farris', + }, + { + id: 'tracking/commissioningdesk/filter-us', + type: 'Tracking', + title: 'The Filter US', + }, + { + id: 'tracking/commissioningdesk/product-review', + type: 'Tracking', + title: 'Product review', + }, + { + id: 'tracking/commissioningdesk/us-filter-product-reviews', + type: 'Tracking', + title: 'US Filter product reviews', + }, + ], + pillar: 'lifestyle', + isLegacyInteractive: false, + isImmersive: false, + sectionLabel: 'Life and style', + sectionUrl: 'lifeandstyle/lifeandstyle', + sectionName: 'thefilter-us', + subMetaSectionLinks: [ + { + url: '/lifeandstyle/lifeandstyle', + title: 'Life and style', + }, + { + url: '/thefilter-us/series/thefilter-us', + title: 'The Filter US', + }, + ], + subMetaKeywordLinks: [ + { + url: '/tone/features', + title: 'features', + }, + ], + shouldHideAds: false, + isAdFreeUser: false, + webURL: 'https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + linkedData: [ + { + '@type': 'NewsArticle', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + publisher: { + '@type': 'Organization', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com#publisher', + name: 'The Guardian', + url: 'https://www.theguardian.com/', + logo: { + '@type': 'ImageObject', + url: 'https://uploads.guim.co.uk/2018/01/31/TheGuardian_AMP.png', + width: 190, + height: 60, + }, + sameAs: [ + 'https://www.facebook.com/theguardian', + 'https://twitter.com/guardian', + 'https://www.youtube.com/user/TheGuardian', + ], + }, + isAccessibleForFree: true, + isPartOf: { + '@type': ['CreativeWork', 'Product'], + name: 'The Guardian', + productID: 'theguardian.com:basic', + }, + image: [ + 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvZmlsdGVyLXVzLnBuZw&enable=upscale&s=e41ac9b32a4532fe5166800799f2d9dc', + 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1200&height=1200&quality=85&auto=format&fit=crop&s=2c68868529022818452dd57a5a5bbc90', + 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1200&height=900&quality=85&auto=format&fit=crop&s=eb44909c78d4e42bfb172e22acc62686', + 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1200&quality=85&auto=format&fit=max&s=a636b166ee0901e896a39f89ddd7c403', + ], + author: [ + { + '@type': 'Person', + name: 'Emily Farris', + sameAs: 'https://www.theguardian.com/profile/emily-farris', + }, + ], + datePublished: '2026-04-03T12:45:31.000Z', + headline: + 'The Anyday glass food containers transformed how I store leftovers', + dateModified: '2026-05-22T16:38:31.000Z', + mainEntityOfPage: + 'https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, + { + '@type': 'WebPage', + '@context': 'https://schema.org', + '@id': 'https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + potentialAction: { + '@type': 'ViewAction', + target: 'android-app://com.guardian/https/www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, + }, + ], + openGraphData: { + 'og:url': + 'https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + 'article:author': 'https://www.theguardian.com/profile/emily-farris', + 'og:image:width': '1200', + 'og:image': + 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvZmlsdGVyLXVzLnBuZw&enable=upscale&s=e41ac9b32a4532fe5166800799f2d9dc', + 'al:ios:url': + 'gnmguardian://thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers?contenttype=Article&source=applinks', + 'article:publisher': 'https://www.facebook.com/theguardian', + 'og:title': + 'Less waste and fewer dishes: these glass food containers changed how I store leftovers', + 'fb:app_id': '180444840287', + 'article:modified_time': '2026-05-22T16:38:31.000Z', + 'og:image:height': '960', + 'og:description': + 'Don’t let leftovers languish in the fridge. These oven, microwave and freezer-safe glass containers from Anyday helped me waste less food – and do fewer dishes', + 'og:type': 'article', + 'al:ios:app_store_id': '409128287', + 'article:section': 'The Filter US', + 'article:published_time': '2026-04-03T12:45:31.000Z', + 'article:tag': 'Life and style', + 'al:ios:app_name': 'The Guardian', + 'og:site_name': 'the Guardian', + }, + twitterData: { + 'twitter:app:id:iphone': '409128287', + 'twitter:app:name:googleplay': 'The Guardian', + 'twitter:app:name:ipad': 'The Guardian', + 'twitter:card': 'summary_large_image', + 'twitter:app:name:iphone': 'The Guardian', + 'twitter:app:id:ipad': '409128287', + 'twitter:app:id:googleplay': 'com.guardian', + 'twitter:app:url:googleplay': + 'guardian://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + 'twitter:app:url:iphone': + 'gnmguardian://thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers?contenttype=Article&source=twitter', + 'twitter:image': + 'https://i.guim.co.uk/img/media/5502d6334997dd30fd616bae9c41f7cbd95042ba/0_0_1728_1382/master/1728.jpg?width=1200&height=630&quality=85&auto=format&fit=crop&precrop=40:21,offset-x50,offset-y0&overlay-align=bottom%2Cleft&overlay-width=100p&overlay-base64=L2ltZy9zdGF0aWMvb3ZlcmxheXMvZmlsdGVyLXVzLnBuZw&s=4b54353ece390f5fcbfcda0e44f6cfc5', + 'twitter:site': '@guardian', + 'twitter:app:url:ipad': + 'gnmguardian://thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers?contenttype=Article&source=twitter', + }, + config: { + references: [ + { + 'rich-link': + 'https://www.theguardian.com/environment/2015/oct/19/sign-up-to-the-green-light-email', + }, + ], + shortUrlId: '/p/d8ex5', + switches: { + prebidAppnexusUkRow: true, + clickToView: true, + prebidTrustx: true, + scAdFreeBanner: false, + compareVariantDecision: false, + enableSentryReporting: true, + lazyLoadContainers: true, + adFreeStrictExpiryEnforcement: false, + liveblogRendering: true, + remarketing: true, + registerWithPhone: false, + targeting: true, + extendedMostPopularFronts: true, + slotBodyEnd: true, + emailInlineInFooter: true, + facebookTrackingPixel: true, + serviceWorkerEnabled: false, + iasAdTargeting: true, + extendedMostPopular: true, + prebidAnalytics: true, + imrWorldwide: true, + acast: true, + twitterUwt: true, + prebidAppnexusInvcode: true, + prebidAppnexus: true, + enableDiscussionSwitch: true, + prebidXaxis: true, + interactiveFullHeaderSwitch: false, + discussionAllPageSize: true, + prebidUserSync: true, + audioOnwardJourneySwitch: true, + mobileStickyPrebid: true, + breakingNews: true, + externalVideoEmbeds: true, + carrotTrafficDriver: true, + geoMostPopular: true, + weAreHiring: true, + relatedContent: true, + thirdPartyEmbedTracking: true, + prebidOzone: true, + mostViewedFronts: true, + abSignInGateMainControl: true, + ampPrebid: true, + googleSearch: true, + brazeSwitch: true, + consentManagement: true, + commercial: true, + prebidSonobi: true, + idProfileNavigation: true, + confiantAdVerification: true, + discussionAllowAnonymousRecommendsSwitch: false, + scrollDepth: true, + permutive: true, + comscore: true, + webFonts: true, + prebidImproveDigital: true, + ophan: true, + crosswordSvgThumbnails: true, + prebidTriplelift: true, + weather: true, + commercialOutbrainNewids: true, + dotcomRendering: true, + abSignInGateMainVariant: true, + hostedVideoAutoplay: true, + abAdblockAsk: true, + prebidPubmatic: true, + autoRefresh: true, + enhanceTweets: true, + prebidIndexExchange: true, + prebidOpenx: true, + idCookieRefresh: true, + sharingComments: true, + abSignInGateMandatory: true, + discussionPageSize: true, + smartAppBanner: false, + boostGaUserTimingFidelity: false, + historyTags: true, + mobileStickyLeaderboard: true, + abDeeplyReadTest: false, + surveys: true, + remoteBanner: true, + inizio: true, + prebidHeaderBidding: true, + a9HeaderBidding: true, + lightbox: true, + }, + keywordIds: + 'environment/climate-change,environment/environment,science/scienceofclimatechange,science/science,world/eu,world/europe-news,world/world,environment/flooding,world/wildfires,world/natural-disasters', + sharedAdTargeting: { + ct: 'article', + co: ['jennifer-rankin'], + url: '/environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + su: ['0'], + edition: 'uk', + tn: ['news'], + p: 'ng', + k: [ + 'eu', + 'flooding', + 'world', + 'europe-news', + 'natural-disasters', + 'science', + 'environment', + 'climate-change', + 'wildfires', + 'scienceofclimatechange', + ], + sh: 'https://www.theguardian.com/p/d8ex5', + }, + toneIds: 'tone/news', + dcrSentryDsn: + 'https://1937ab71c8804b2b8438178dfdd6468f@sentry.io/1377847', + discussionApiUrl: 'https://discussion.theguardian.com/discussion-api', + sentryPublicApiKey: '344003a8d11c41d8800fbad8383fdc50', + commercialBundleUrl: + 'https://assets.guim.co.uk/javascripts/bc58c17d75809551440f/graun.commercial.dcr.js', + discussionApiClientHeader: 'nextgen', + shouldHideReaderRevenue: false, + sentryHost: 'app.getsentry.com/35463', + isPaidContent: false, + headline: 'Headline string', + idApiUrl: 'https://idapi.theguardian.com', + showRelatedContent: true, + adUnit: '/59666047/theguardian.com/environment/article/ng', + videoDuration: 0, + stage: 'PROD', + isSensitive: false, + isDev: false, + ajaxUrl: 'https://api.nextgen.guardianapps.co.uk', + keywords: + 'Climate change,Environment,Climate change,Science,European Union,Europe,World news,Flooding,Wildfires,Natural disasters and extreme weather', + revisionNumber: 'DEV', + section: 'environment', + isPhotoEssay: false, + ampIframeUrl: + 'https://assets.guim.co.uk/data/vendor/b242a49b1588bb36bdaacefe001ca77a/amp-iframe.html', + isLive: false, + host: 'https://www.theguardian.com', + brazeApiKey: '7f28c639-8bda-48ff-a3f6-24345abfc07c', + contentType: 'Article', + idUrl: 'https://profile.theguardian.com', + author: 'Jennifer Rankin', + dfpAccountId: '59666047', + pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', + googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', + mmaUrl: 'https://manage.theguardian.com', + serverSideABTests: {}, + edition: 'UK', + ipsosTag: 'environment', + isLiveBlog: false, + frontendAssetsFullURL: 'https://assets.guim.co.uk/', + webPublicationDate: 1581314427000, + discussionD2Uid: 'zHoBy6HNKsk', + }, + guardianBaseURL: 'https://www.theguardian.com', + contentType: 'Article', + hasRelated: true, + hasStoryPackage: false, + beaconURL: '//phar.gu-web.net', + isCommentable: false, + commercialProperties: { + US: { + adTargeting: [ + { + name: 'co', + value: ['emily-farris'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'tn', + value: ['features', 'best-ofs', 'reviews'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter-us', + }, + { + name: 'url', + value: '/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4xq7e', + }, + { + name: 'edition', + value: 'us', + }, + { + name: 'se', + value: [ + 'kitchen-aide', + 'thefilter-us', + 'plastic-free-life', + ], + }, + ], + }, + AU: { + adTargeting: [ + { + name: 'co', + value: ['emily-farris'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'edition', + value: 'au', + }, + { + name: 'se', + value: [ + 'kitchen-aide', + 'thefilter-us', + 'plastic-free-life', + ], + }, + { + name: 'tn', + value: ['features', 'best-ofs', 'reviews'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter-us', + }, + { + name: 'url', + value: '/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4xq7e', + }, + ], + }, + UK: { + adTargeting: [ + { + name: 'co', + value: ['emily-farris'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'se', + value: [ + 'kitchen-aide', + 'thefilter-us', + 'plastic-free-life', + ], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter-us', + }, + { + name: 'url', + value: '/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4xq7e', + }, + { + name: 'tn', + value: ['features', 'best-ofs', 'reviews'], + }, + { + name: 'edition', + value: 'uk', + }, + ], + }, + INT: { + adTargeting: [ + { + name: 'co', + value: ['emily-farris'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'se', + value: [ + 'kitchen-aide', + 'thefilter-us', + 'plastic-free-life', + ], + }, + { + name: 'tn', + value: ['features', 'best-ofs', 'reviews'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 's', + value: 'thefilter-us', + }, + { + name: 'url', + value: '/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, + { + name: 'edition', + value: 'int', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4xq7e', + }, + ], + }, + EUR: { + adTargeting: [ + { + name: 'co', + value: ['emily-farris'], + }, + { + name: 'sens', + value: 'f', + }, + { + name: 'se', + value: [ + 'kitchen-aide', + 'thefilter-us', + 'plastic-free-life', + ], + }, + { + name: 'tn', + value: ['features', 'best-ofs', 'reviews'], + }, + { + name: 'k', + value: ['lifeandstyle'], + }, + { + name: 'ct', + value: 'article', + }, + { + name: 'url', + value: '/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, + { + name: 'p', + value: 'ng', + }, + { + name: 'su', + value: ['0'], + }, + { + name: 'sh', + value: 'https://www.theguardian.com/p/x4xq7e', + }, + { + name: 's', + value: 'thefilter-us', + }, + { + name: 'edition', + value: 'eur', + }, + ], + }, + }, + pageType: { + hasShowcaseMainElement: false, + isFront: false, + isLiveblog: false, + isMinuteArticle: false, + isPaidContent: false, + isPreview: false, + isSensitive: false, + }, + starRating: 5, + trailText: + 'Don’t let leftovers languish in the fridge. These oven, microwave and freezer-safe glass containers from Anyday helped me waste less food – and do fewer dishes', + nav: { + currentUrl: '/thefilter-us', + pillars: [ + { + title: 'News', + url: '/', + longTitle: 'Headlines', + iconName: 'home', + children: [ + { + title: 'UK', + url: '/uk-news', + longTitle: 'UK news', + children: [ + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Education', + url: '/education', + children: [ + { + title: 'Schools', + url: '/education/schools', + }, + { + title: 'Teachers', + url: '/teacher-network', + }, + { + title: 'Universities', + url: '/education/universities', + }, + { + title: 'Students', + url: '/education/students', + }, + ], + }, + { + title: 'Media', + url: '/media', + }, + { + title: 'Society', + url: '/society', + }, + { + title: 'Law', + url: '/law', + }, + { + title: 'Scotland', + url: '/uk/scotland', + }, + { + title: 'Wales', + url: '/uk/wales', + }, + { + title: 'Northern Ireland', + url: '/uk/northernireland', + }, + ], + }, + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'US politics', + url: '/us-news/us-politics', + }, + { + title: 'World', + url: '/world', + longTitle: 'World news', + children: [ + { + title: 'Europe', + url: '/world/europe-news', + }, + { + title: 'US news', + url: '/us-news', + longTitle: 'US news', + }, + { + title: 'Americas', + url: '/world/americas', + }, + { + title: 'Asia', + url: '/world/asia', + }, + { + title: 'Australia', + url: '/australia-news', + longTitle: 'Australia news', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Africa', + url: '/world/africa', + }, + { + title: 'Inequality', + url: '/inequality', + }, + { + title: 'Global development', + url: '/global-development', + }, + ], + }, + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Ukraine', + url: '/world/ukraine', + }, + { + title: 'Football', + url: '/football', + children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: 'Business', + url: '/business', + children: [ + { + title: 'Economics', + url: '/business/economics', + }, + { + title: 'Banking', + url: '/business/banking', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Markets', + url: '/business/stock-markets', + }, + { + title: 'Project Syndicate', + url: '/business/series/project-syndicate-economists', + }, + { + title: 'B2B', + url: '/business-to-business', + }, + { + title: 'Retail', + url: '/business/retail', + }, + ], + }, + { + title: 'Environment', + url: '/environment', + children: [ + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Wildlife', + url: '/environment/wildlife', + }, + { + title: 'Energy', + url: '/environment/energy', + }, + { + title: 'Pollution', + url: '/environment/pollution', + }, + ], + }, + { + title: 'UK politics', + url: '/politics', + }, + { + title: 'Science', + url: '/science', + }, + { + title: 'Tech', + url: '/technology', + }, + { + title: 'Global development', + url: '/global-development', + }, + { + title: 'Obituaries', + url: '/obituaries', + }, + ], + }, + { + title: 'Opinion', + url: '/commentisfree', + longTitle: 'Opinion home', + iconName: 'home', + children: [ + { + title: 'The Guardian view', + url: '/profile/editorial', + }, + { + title: 'Columnists', + url: '/uk/columnists', + }, + { + title: 'Cartoons', + url: '/tone/cartoons', + }, + { + title: 'Opinion videos', + url: '/type/video+tone/comment', + }, + { + title: 'Letters', + url: '/tone/letters', + }, + ], + }, + { + title: 'Sport', + url: '/sport', + longTitle: 'Sport home', + iconName: 'home', + children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'Football', + url: '/football', + children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'Live scores', + url: '/football/live', + longTitle: 'football/live', + }, + { + title: 'Tables', + url: '/football/tables', + longTitle: 'football/tables', + }, + { + title: 'Fixtures', + url: '/football/fixtures', + longTitle: 'football/fixtures', + }, + { + title: 'Results', + url: '/football/results', + longTitle: 'football/results', + }, + { + title: 'Competitions', + url: '/football/competitions', + longTitle: 'football/competitions', + }, + { + title: 'Clubs', + url: '/football/teams', + longTitle: 'football/teams', + }, + ], + }, + { + title: 'Cricket', + url: '/sport/cricket', + }, + { + title: 'Rugby union', + url: '/sport/rugby-union', + }, + { + title: 'Tennis', + url: '/sport/tennis', + }, + { + title: 'Cycling', + url: '/sport/cycling', + }, + { + title: 'F1', + url: '/sport/formulaone', + }, + { + title: 'Golf', + url: '/sport/golf', + }, + { + title: 'Boxing', + url: '/sport/boxing', + }, + { + title: 'Rugby league', + url: '/sport/rugbyleague', + }, + { + title: 'Racing', + url: '/sport/horse-racing', + }, + { + title: 'US sports', + url: '/sport/us-sport', + }, + ], + }, + { + title: 'Culture', + url: '/culture', + longTitle: 'Culture home', + iconName: 'home', + children: [ + { + title: 'Film', + url: '/film', + }, + { + title: 'Music', + url: '/music', + }, + { + title: 'TV & radio', + url: '/tv-and-radio', + }, + { + title: 'Books', + url: '/books', + }, + { + title: 'Art & design', + url: '/artanddesign', + }, + { + title: 'Stage', + url: '/stage', + }, + { + title: 'Games', + url: '/games', + }, + { + title: 'Classical', + url: '/music/classicalmusicandopera', + }, + ], + }, + { + title: 'Lifestyle', + url: '/lifeandstyle', + longTitle: 'Lifestyle home', + iconName: 'home', + children: [ + { + title: 'The Filter', + url: '/uk/thefilter', + }, + { + title: 'Fashion', + url: '/fashion', + }, + { + title: 'Food', + url: '/food', + }, + { + title: 'Recipes', + url: '/tone/recipes', + }, + { + title: 'Travel', + url: '/travel', + children: [ + { + title: 'UK', + url: '/travel/uk', + }, + { + title: 'Europe', + url: '/travel/europe', + }, + { + title: 'US', + url: '/travel/usa', + }, + ], + }, + { + title: 'Health & fitness', + url: '/lifeandstyle/health-and-wellbeing', + }, + { + title: 'Women', + url: '/lifeandstyle/women', + }, + { + title: 'Men', + url: '/lifeandstyle/men', + }, + { + title: 'Love & sex', + url: '/lifeandstyle/love-and-sex', + }, + { + title: 'Beauty', + url: '/fashion/beauty', + }, + { + title: 'Home & garden', + url: '/lifeandstyle/home-and-garden', + }, + { + title: 'Money', + url: '/money', + children: [ + { + title: 'Property', + url: '/money/property', + }, + { + title: 'Pensions', + url: '/money/pensions', + }, + { + title: 'Savings', + url: '/money/savings', + }, + { + title: 'Borrowing', + url: '/money/debt', + }, + { + title: 'Careers', + url: '/money/work-and-careers', + }, + ], + }, + { + title: 'Cars', + url: '/technology/motoring', + }, + ], + }, + ], + otherLinks: [ + { + title: 'The Guardian app', + url: 'https://app.adjust.com/16xt6hai', + }, + { + title: 'Video', + url: '/video', + }, + { + title: 'Podcasts', + url: '/podcasts', + }, + { + title: 'Pictures', + url: '/inpictures', + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: "Today's paper", + url: '/theguardian', + children: [ + { + title: 'Obituaries', + url: '/obituaries', + }, + { + title: 'G2', + url: '/theguardian/g2', + }, + { + title: 'Journal', + url: '/theguardian/journal', + }, + { + title: 'Saturday', + url: '/theguardian/saturday', + }, + ], + }, + { + title: 'Inside the Guardian', + url: 'https://www.theguardian.com/insidetheguardian', + }, + { + title: 'Guardian Weekly', + url: 'https://www.theguardian.com/weekly?INTCMP=gdnwb_mawns_editorial_gweekly_GW_TopNav_UK', + }, + { + title: 'Crosswords', + url: '/crosswords', + children: [ + { + title: 'Blog', + url: '/crosswords/crossword-blog', + }, + { + title: 'Quick', + url: '/crosswords/series/quick', + }, + { + title: 'Sunday quick', + url: '/crosswords/series/sunday-quick', + }, + { + title: 'Mini', + url: '/crosswords/series/mini-crossword', + }, + { + title: 'Quick cryptic', + url: '/crosswords/series/quick-cryptic', + }, + { + title: 'Quiptic', + url: '/crosswords/series/quiptic', + }, + { + title: 'Cryptic', + url: '/crosswords/series/cryptic', + }, + { + title: 'Prize', + url: '/crosswords/series/prize', + }, + { + title: 'Genius', + url: '/crosswords/series/genius', + }, + { + title: 'Weekend', + url: '/crosswords/series/weekend-crossword', + }, + { + title: 'Special', + url: '/crosswords/series/special', + }, + ], + }, + { + title: 'Wordiply', + url: 'https://www.wordiply.com', + }, + { + title: 'Corrections', + url: '/theguardian/series/corrections-and-clarifications', + }, + { + title: 'Tips', + url: 'https://www.theguardian.com/tips', + }, + ], + brandExtensions: [ + { + title: 'Search jobs', + url: 'https://jobs.theguardian.com', + }, + { + title: 'Hire with Guardian Jobs', + url: 'https://recruiters.theguardian.com/?utm_source=gdnwb&utm_medium=navbar&utm_campaign=Guardian_Navbar_Recruiters&CMP_TU=trdmkt&CMP_BUNIT=jobs', + }, + { + title: 'Holidays', + url: 'https://holidays.theguardian.com?INTCMP=holidays_uk_web_newheader', + }, + { + title: 'Live events', + url: 'https://www.theguardian.com/guardian-live-events?INTCMP=live_uk_header_dropdown', + }, + { + title: 'About Us', + url: '/about', + }, + { + title: 'Digital Archive', + url: 'https://theguardian.newspapers.com', + }, + { + title: 'Guardian Print Shop', + url: '/artanddesign/series/gnm-print-sales', + }, + { + title: 'Patrons', + url: 'https://patrons.theguardian.com/?INTCMP=header_patrons', + }, + { + title: 'Guardian Licensing', + url: 'https://licensing.theguardian.com/', + }, + ], + currentNavLinkTitle: 'The Filter', + currentPillarTitle: 'News', + subNavSections: { + links: [ + { + title: 'US news', + url: '/us-news', + longTitle: 'US news', + }, + { + title: 'US politics', + url: '/us-news/us-politics', + }, + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, + { + title: 'World', + url: '/world', + longTitle: 'World news', + children: [ + { + title: 'Europe', + url: '/world/europe-news', + }, + { + title: 'US news', + url: '/us-news', + longTitle: 'US news', + }, + { + title: 'Americas', + url: '/world/americas', + }, + { + title: 'Asia', + url: '/world/asia', + }, + { + title: 'Australia', + url: '/australia-news', + longTitle: 'Australia news', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Africa', + url: '/world/africa', + }, + { + title: 'Inequality', + url: '/inequality', + }, + { + title: 'Global development', + url: '/global-development', + }, + ], + }, + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Middle East', + url: '/world/middleeast', + }, + { + title: 'Ukraine', + url: '/world/ukraine', + }, + { + title: 'US immigration', + url: '/us-news/usimmigration', + }, + { + title: 'Business', + url: '/us/business', + children: [ + { + title: 'Economics', + url: '/business/economics', + }, + { + title: 'Diversity & equality in business', + url: '/business/diversity-and-equality', + }, + { + title: 'Small business', + url: '/business/us-small-business', + }, + { + title: 'Retail', + url: '/business/retail', + }, + ], + }, + { + title: 'Environment', + url: '/us/environment', + children: [ + { + title: 'Climate crisis', + url: '/environment/climate-crisis', + }, + { + title: 'Wildlife', + url: '/environment/wildlife', + }, + { + title: 'Energy', + url: '/environment/energy', + }, + { + title: 'Pollution', + url: '/environment/pollution', + }, + { + title: 'Green light', + url: '/environment/series/green-light', + }, + ], + }, + { + title: 'Tech', + url: '/us/technology', + }, + { + title: 'Science', + url: '/science', + }, + { + title: 'Newsletters', + url: '/email-newsletters', + }, + { + title: 'The Filter', + url: '/thefilter-us', + }, + { + title: 'Wellness', + url: '/us/wellness', + }, + ], + }, + readerRevenueLinks: { + header: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + footer: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_supporter_cta%22%7D', + }, + sideMenu: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=side_menu_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=side_menu_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=side_menu_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22side_menu_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=mobilenav_print_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22mobilenav_print_cta%22%7D', + }, + ampHeader: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=header_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=header_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=amp_header_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22amp_header_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=header_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_HEADER%22,%22componentId%22:%22header_supporter_cta%22%7D', + }, + ampFooter: { + contribute: + 'https://support.theguardian.com/contribute?INTCMP=amp_footer_support_contribute&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_contribute%22%7D', + subscribe: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_support_subscribe&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_support_subscribe%22%7D', + support: + 'https://support.theguardian.com?INTCMP=footer_support&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22footer_support%22%7D', + supporter: + 'https://support.theguardian.com/subscribe?INTCMP=amp_footer_supporter_cta&acquisitionData=%7B%22source%22:%22GUARDIAN_WEB%22,%22componentType%22:%22ACQUISITIONS_FOOTER%22,%22componentId%22:%22amp_footer_supporter_cta%22%7D', + }, + }, + }, + showBottomSocialButtons: true, + pageFooter: { + footerLinks: [ + [ + { + text: 'About us', + url: '/about', + dataLinkName: 'uk : footer : about us', + extraClasses: '', + }, + { + text: 'Help', + url: 'https://manage.theguardian.com/help-centre', + dataLinkName: 'uk : footer : tech feedback', + extraClasses: 'js-tech-feedback-report', + }, + { + text: 'Complaints & corrections', + url: '/info/complaints-and-corrections', + dataLinkName: 'complaints', + extraClasses: '', + }, + { + text: 'Contact us', + url: '/help/contact-us', + dataLinkName: 'uk : footer : contact us', + extraClasses: '', + }, + { + text: 'Tip us off', + url: 'https://www.theguardian.com/tips', + dataLinkName: 'uk : footer : tips', + extraClasses: '', + }, + { + text: 'SecureDrop', + url: 'https://www.theguardian.com/securedrop', + dataLinkName: 'securedrop', + extraClasses: '', + }, + { + text: 'Privacy policy', + url: '/info/privacy', + dataLinkName: 'privacy', + extraClasses: '', + }, + { + text: 'Cookie policy', + url: '/info/cookies', + dataLinkName: 'cookie', + extraClasses: '', + }, + { + text: 'Modern Slavery Act', + url: 'https://uploads.guim.co.uk/2025/09/05/Modern_Slavery_Statement_2025.pdf', + dataLinkName: 'uk : footer : modern slavery act statement', + extraClasses: '', + }, + { + text: 'Tax strategy', + url: 'https://uploads.guim.co.uk/2025/09/05/Tax_strategy_for_the_year_ended_31_March_2025.pdf', + dataLinkName: 'uk : footer : tax strategy', + extraClasses: '', + }, + { + text: 'Terms & conditions', + url: '/help/terms-of-service', + dataLinkName: 'terms', + extraClasses: '', + }, + ], + [ + { + text: 'All topics', + url: '/index/subjects/a', + dataLinkName: 'uk : footer : all topics', + extraClasses: '', + }, + { + text: 'All writers', + url: '/index/contributors', + dataLinkName: 'uk : footer : all contributors', + extraClasses: '', + }, + { + text: 'Newsletters', + url: '/email-newsletters?INTCMP=DOTCOM_FOOTER_NEWSLETTER_UK', + dataLinkName: 'uk : footer : newsletters', + extraClasses: '', + }, + { + text: 'Digital newspaper archive', + url: 'https://theguardian.newspapers.com', + dataLinkName: 'digital newspaper archive', + extraClasses: '', + }, + { + text: 'Bluesky', + url: 'https://bsky.app/profile/theguardian.com', + dataLinkName: 'uk : footer : Bluesky', + extraClasses: '', + }, + { + text: 'Facebook', + url: 'https://www.facebook.com/theguardian', + dataLinkName: 'uk : footer : Facebook', + extraClasses: '', + }, + { + text: 'Instagram', + url: 'https://www.instagram.com/guardian', + dataLinkName: 'uk : footer : Instagram', + extraClasses: '', + }, + { + text: 'LinkedIn', + url: 'https://www.linkedin.com/company/theguardian', + dataLinkName: 'uk : footer : LinkedIn', + extraClasses: '', + }, + { + text: 'Threads', + url: 'https://www.threads.com/@guardian', + dataLinkName: 'uk : footer : Threads', + extraClasses: '', + }, + { + text: 'TikTok', + url: 'https://www.tiktok.com/@guardian', + dataLinkName: 'uk : footer : TikTok', + extraClasses: '', + }, + { + text: 'YouTube', + url: 'https://www.youtube.com/user/TheGuardian', + dataLinkName: 'uk : footer : YouTube', + extraClasses: '', + }, + ], + [ + { + text: 'Advertise with us', + url: 'https://advertising.theguardian.com', + dataLinkName: 'uk : footer : advertise with us', + extraClasses: '', + }, + { + text: 'Guardian Labs', + url: '/guardian-labs', + dataLinkName: 'uk : footer : guardian labs', + extraClasses: '', + }, + { + text: 'Search jobs', + url: 'https://jobs.theguardian.com', + dataLinkName: 'uk : footer : jobs', + extraClasses: '', + }, + { + text: 'Patrons', + url: 'https://patrons.theguardian.com?INTCMP=footer_patrons', + dataLinkName: 'uk : footer : patrons', + extraClasses: '', + }, + { + text: 'Work with us', + url: 'https://workwithus.theguardian.com/', + dataLinkName: 'uk : footer : work with us', + extraClasses: '', + }, + { + text: 'Accessibility settings', + url: '/help/accessibility-help', + dataLinkName: 'accessibility settings', + extraClasses: '', + }, + ], + ], + }, + publication: 'theguardian.com', + shouldHideReaderRevenue: false, + slotMachineFlags: '', + contributionsServiceUrl: 'https://contributions.guardianapis.com', + isSpecialReport: false, + promotedNewsletter: { + identityName: 'the-filter-us', + name: 'The Filter US', + theme: 'lifestyle', + description: 'A guide to buying fewer, better products.', + frequency: 'Weekly', + listId: 6057, + group: 'Lifestyle', + successDescription: 'You are subscribed', + regionFocus: 'US', + illustrationCard: + 'https://uploads.guim.co.uk/2024/10/04/The_Filter_-_5-3.jpg', + illustrationSquare: + 'https://media.guim.co.uk/c8a44e1d3ffcbf49017e464b41e9c7b31139e713/926_0_379_379/379.jpg', + exampleUrl: 'lifeandstyle/series/the-filter-us-newsletter/latest', + category: 'article-based', + }, + showTableOfContents: true, + lang: 'en', + isRightToLeftLang: false, +}; diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Analysis.ts b/dotcom-rendering/fixtures/generated/fe-articles/Analysis.ts index 3fb62da022b..52b3f8b3b18 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Analysis.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Analysis.ts @@ -815,7 +815,6 @@ export const Analysis: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts b/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts index 466858bc773..44c9b170cd8 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Audio.ts @@ -456,7 +456,6 @@ export const Audio: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Comment.ts b/dotcom-rendering/fixtures/generated/fe-articles/Comment.ts index aef685a4171..ebe2a4af3b6 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Comment.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Comment.ts @@ -783,7 +783,6 @@ export const Comment: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Dead.ts b/dotcom-rendering/fixtures/generated/fe-articles/Dead.ts index 6efb77ee3da..4be46376d28 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Dead.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Dead.ts @@ -3192,7 +3192,6 @@ export const Dead: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Editorial.ts b/dotcom-rendering/fixtures/generated/fe-articles/Editorial.ts index cb9cc02f75c..1d33ed4b124 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Editorial.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Editorial.ts @@ -763,7 +763,6 @@ export const Editorial: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Explainer.ts b/dotcom-rendering/fixtures/generated/fe-articles/Explainer.ts index 868998e9b7c..da2da9e1075 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Explainer.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Explainer.ts @@ -1540,7 +1540,6 @@ export const Explainer: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Feature.ts b/dotcom-rendering/fixtures/generated/fe-articles/Feature.ts index 992f19b070d..93856ef3154 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Feature.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Feature.ts @@ -1267,7 +1267,6 @@ export const Feature: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Gallery.ts b/dotcom-rendering/fixtures/generated/fe-articles/Gallery.ts index 578e6faf7c2..4416e89d9c5 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Gallery.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Gallery.ts @@ -6806,7 +6806,6 @@ export const Gallery: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/GalleryLabs.ts b/dotcom-rendering/fixtures/generated/fe-articles/GalleryLabs.ts index cabd3c9e8ae..5c4baec4407 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/GalleryLabs.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/GalleryLabs.ts @@ -4405,7 +4405,6 @@ export const GalleryLabs: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Interview.ts b/dotcom-rendering/fixtures/generated/fe-articles/Interview.ts index 59fcc7b0d58..709d7549df6 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Interview.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Interview.ts @@ -2382,7 +2382,6 @@ export const Interview: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Labs.ts b/dotcom-rendering/fixtures/generated/fe-articles/Labs.ts index 19d2619ada4..1abe70eaba5 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Labs.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Labs.ts @@ -1761,7 +1761,6 @@ export const Labs: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Letter.ts b/dotcom-rendering/fixtures/generated/fe-articles/Letter.ts index ef06d0d208b..f76ea6dfd36 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Letter.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Letter.ts @@ -659,7 +659,6 @@ export const Letter: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Live.ts b/dotcom-rendering/fixtures/generated/fe-articles/Live.ts index 5a2b7fe8d81..a1d248b03f7 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Live.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Live.ts @@ -3192,7 +3192,6 @@ export const Live: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/LiveBlogSingleContributor.ts b/dotcom-rendering/fixtures/generated/fe-articles/LiveBlogSingleContributor.ts index 645dbdfc519..73a54db8c66 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/LiveBlogSingleContributor.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/LiveBlogSingleContributor.ts @@ -4389,7 +4389,6 @@ export const LiveBlogSingleContributor: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/MatchReport.ts b/dotcom-rendering/fixtures/generated/fe-articles/MatchReport.ts index 35aadabedaf..063b5ad3759 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/MatchReport.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/MatchReport.ts @@ -747,7 +747,6 @@ export const MatchReport: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/NewsletterSignup.ts b/dotcom-rendering/fixtures/generated/fe-articles/NewsletterSignup.ts index 15c17a787ef..f4e95dfe86d 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/NewsletterSignup.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/NewsletterSignup.ts @@ -702,7 +702,6 @@ export const NewsletterSignup: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/NumberedList.ts b/dotcom-rendering/fixtures/generated/fe-articles/NumberedList.ts index f92c4c83e06..8d676f30b8e 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/NumberedList.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/NumberedList.ts @@ -5917,7 +5917,6 @@ export const NumberedList: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/PhotoEssay.ts b/dotcom-rendering/fixtures/generated/fe-articles/PhotoEssay.ts index e25111f9156..d47cae8364a 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/PhotoEssay.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/PhotoEssay.ts @@ -7780,7 +7780,6 @@ export const PhotoEssay: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Picture.ts b/dotcom-rendering/fixtures/generated/fe-articles/Picture.ts index 593d47dcbfc..7c51ebca787 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Picture.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Picture.ts @@ -663,7 +663,6 @@ export const Picture: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Quiz.ts b/dotcom-rendering/fixtures/generated/fe-articles/Quiz.ts index 0f137d83545..3fbbba21e10 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Quiz.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Quiz.ts @@ -1263,7 +1263,6 @@ export const Quiz: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Recipe.ts b/dotcom-rendering/fixtures/generated/fe-articles/Recipe.ts index 23e2a439f2d..085fad78934 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Recipe.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Recipe.ts @@ -286,6 +286,20 @@ export const Recipe: FEArticle = { html: '

Spring onion pancakes with sesame sauce

', elementId: '6338873b-49fd-48e4-9851-fefd51c92dab', }, + { + _type: 'model.dotcomrendering.pageElements.RecipeBlockElement', + id: 'e5a2cb2a63b788eae68ff654f739eff53a0cee28', + title: 'Spring onion pancakes with sesame sauce', + description: + "The world of pancakes is so vast, it is hard to think that on Pancake Day, there could be only one type proffered across the world. This offering is for cong you bing, a flaky, coiled, spring onion pancake ubiquitous across China. It's as enjoyable to make as it is to eat and, happily, there's no whiff of abstinence about it.", + featuredImage: { + url: 'https://i.guim.co.uk/img/media/9dcb491db0315d5598fc47aa51d049e12eedcbbc/0_18_2831_3539/master/2831.jpg?width=620&dpr=2&s=none&crop=none', + mediaId: 'e5a2cb2a63b788eae68ff654f739eff53a0cee28', + cropId: '0_0_3731_4384', + caption: + "Meera Sodha's spring onion pancakes with sesame sauce", + }, + }, { _type: 'model.dotcomrendering.pageElements.TextBlockElement', html: '

Prep 5 min
Rest 30 min
Cook 1 hr
Makes 4, to serve 2 for lunch

', @@ -848,7 +862,6 @@ export const Recipe: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Review.ts b/dotcom-rendering/fixtures/generated/fe-articles/Review.ts index 11aa501a928..6c644add14a 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Review.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Review.ts @@ -1092,7 +1092,6 @@ export const Review: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/SpecialReport.ts b/dotcom-rendering/fixtures/generated/fe-articles/SpecialReport.ts index 8f49d377d33..d60ae77269c 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/SpecialReport.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/SpecialReport.ts @@ -862,7 +862,6 @@ export const SpecialReport: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Standard.ts b/dotcom-rendering/fixtures/generated/fe-articles/Standard.ts index e399a8df4d3..8bc3c2b9a94 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Standard.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Standard.ts @@ -833,7 +833,6 @@ export const Standard: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/StandardWithVideo.ts b/dotcom-rendering/fixtures/generated/fe-articles/StandardWithVideo.ts index 1f7da86a84a..6694d6611cc 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/StandardWithVideo.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/StandardWithVideo.ts @@ -833,7 +833,6 @@ export const StandardWithVideo: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/fe-articles/Video.ts b/dotcom-rendering/fixtures/generated/fe-articles/Video.ts index fc45b21463d..7ede28f0f9b 100644 --- a/dotcom-rendering/fixtures/generated/fe-articles/Video.ts +++ b/dotcom-rendering/fixtures/generated/fe-articles/Video.ts @@ -401,7 +401,6 @@ export const Video: FEArticle = { pageId: 'environment/2020/feb/10/fires-floods-maps-europe-climate-catastrophe', googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', mmaUrl: 'https://manage.theguardian.com', - abTests: {}, serverSideABTests: {}, edition: 'UK', ipsosTag: 'environment', diff --git a/dotcom-rendering/fixtures/generated/football-live.ts b/dotcom-rendering/fixtures/generated/football-live.ts index 349c3f51162..96b5fc8bba6 100644 --- a/dotcom-rendering/fixtures/generated/football-live.ts +++ b/dotcom-rendering/fixtures/generated/football-live.ts @@ -1048,9 +1048,6 @@ export const footballData: FEFootballMatchListPage = { europeBetaFront: true, prebidBidCache: true, }, - abTests: { - europeBetaFrontTest2Variant: 'variant', - }, serverSideABTests: {}, googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', stage: 'PROD', diff --git a/dotcom-rendering/fixtures/manual/cricket-scoreboard.ts b/dotcom-rendering/fixtures/manual/cricket-scoreboard.ts index 9159e390b7d..cdb85230fe5 100644 --- a/dotcom-rendering/fixtures/manual/cricket-scoreboard.ts +++ b/dotcom-rendering/fixtures/manual/cricket-scoreboard.ts @@ -142,7 +142,8 @@ export const match: CricketMatch = { fallOfWicket: [], }, ], - competitionName: 'Third Test Match', + competitionName: 'Test Match Series', + stage: 'Third Test Match', venueName: 'National Cricket Stadium', gameDate: '2022-03-24T14:00:00', matchId: '0fddabb8-a5ce-eef6-bfbd-e80d1edc7805', diff --git a/dotcom-rendering/fixtures/manual/cricketMatch.ts b/dotcom-rendering/fixtures/manual/cricketMatch.ts new file mode 100644 index 00000000000..08d7e0dfffc --- /dev/null +++ b/dotcom-rendering/fixtures/manual/cricketMatch.ts @@ -0,0 +1,666 @@ +import type { FECricketMatch } from '../../src/frontend/feCricketMatchPage'; + +export const liveMatch: FECricketMatch = { + teams: [ + { + name: 'England', + id: 'a359844f-fc07-9cfa-d4cc-9a9ac0d5d075', + home: true, + lineup: [ + 'Ben Duckett', + 'Emilio Gay', + 'Jacob Bethell', + 'Harry Brook', + 'Joe Root', + 'James Rew', + 'Jordan Cox', + 'Jofra Archer', + 'Josh Tongue', + 'Matthew Fisher', + 'Sonny Baker', + ], + teamTagId: 'sport/england-cricket-team', + }, + { + name: 'New Zealand', + id: '110c70b5-c05f-3be7-6670-baecd50a8c6b', + home: false, + lineup: [ + 'Tom Latham', + 'Devon Conway', + 'Henry Nicholls', + 'Rachin Ravindra', + 'Daryl Mitchell', + 'Tom Blundell', + 'Glenn Phillips', + 'Nathan Smith', + 'Kyle Jamieson', + "Will O'Rourke", + 'Matt Henry', + ], + teamTagId: 'sport/new-zealand-cricket-team', + }, + ], + innings: [ + { + order: 1, + battingTeam: 'New Zealand', + runsScored: 391, + wickets: 10, + overs: '96.2', + declared: false, + forfeited: false, + description: 'New Zealand first innings', + batters: [ + { + name: 'Tom Latham', + order: 1, + ballsFaced: 75, + runs: 27, + fours: 1, + sixes: 0, + out: true, + howOut: 'c Bethell b Archer', + onStrike: false, + nonStrike: true, + }, + { + name: 'Devon Conway', + order: 2, + ballsFaced: 15, + runs: 9, + fours: 1, + sixes: 0, + out: true, + howOut: 'c Rew b Fisher', + onStrike: false, + nonStrike: true, + }, + { + name: 'Henry Nicholls', + order: 3, + ballsFaced: 57, + runs: 24, + fours: 3, + sixes: 0, + out: true, + howOut: 'b Tongue', + onStrike: false, + nonStrike: true, + }, + { + name: 'Rachin Ravindra', + order: 4, + ballsFaced: 51, + runs: 33, + fours: 6, + sixes: 0, + out: true, + howOut: 'c Bethell b Baker', + onStrike: false, + nonStrike: true, + }, + { + name: 'Daryl Mitchell', + order: 5, + ballsFaced: 74, + runs: 44, + fours: 6, + sixes: 0, + out: true, + howOut: 'c Gay b Baker', + onStrike: false, + nonStrike: true, + }, + { + name: 'Tom Blundell', + order: 6, + ballsFaced: 84, + runs: 51, + fours: 6, + sixes: 0, + out: true, + howOut: 'c Root b Bethell', + onStrike: false, + nonStrike: true, + }, + { + name: 'Glenn Phillips', + order: 7, + ballsFaced: 135, + runs: 100, + fours: 18, + sixes: 0, + out: true, + howOut: 'c Gay b Fisher', + onStrike: false, + nonStrike: true, + }, + { + name: 'Nathan Smith', + order: 8, + ballsFaced: 21, + runs: 4, + fours: 0, + sixes: 0, + out: true, + howOut: 'c Cox b Bethell', + onStrike: false, + nonStrike: true, + }, + { + name: 'Kyle Jamieson', + order: 9, + ballsFaced: 48, + runs: 41, + fours: 7, + sixes: 0, + out: true, + howOut: 'b Bethell', + onStrike: false, + nonStrike: true, + }, + { + name: 'Matt Henry', + order: 10, + ballsFaced: 19, + runs: 5, + fours: 1, + sixes: 0, + out: true, + howOut: 'c Tongue b Archer', + onStrike: false, + nonStrike: true, + }, + { + name: "Will O'Rourke", + order: 11, + ballsFaced: 2, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Not Out', + onStrike: false, + nonStrike: false, + }, + ], + bowlers: [ + { + name: 'Jofra Archer', + order: 1, + overs: 20, + maidens: 4, + runs: 61, + wickets: 2, + balls: 120, + }, + { + name: 'Matthew Fisher', + order: 2, + overs: 23, + maidens: 6, + runs: 62, + wickets: 2, + balls: 140, + }, + { + name: 'Josh Tongue', + order: 3, + overs: 21, + maidens: 1, + runs: 97, + wickets: 1, + balls: 126, + }, + { + name: 'Sonny Baker', + order: 4, + overs: 19, + maidens: 2, + runs: 94, + wickets: 2, + balls: 114, + }, + { + name: 'Jacob Bethell', + order: 6, + overs: 10, + maidens: 1, + runs: 26, + wickets: 3, + balls: 60, + }, + ], + fallOfWicket: [ + { + order: 1, + name: 'Devon Conway', + runs: 14, + }, + { + order: 2, + name: 'Tom Latham', + runs: 58, + }, + { + order: 3, + name: 'Henry Nicholls', + runs: 79, + }, + { + order: 4, + name: 'Rachin Ravindra', + runs: 107, + }, + { + order: 5, + name: 'Daryl Mitchell', + runs: 188, + }, + { + order: 6, + name: 'Tom Blundell', + runs: 263, + }, + { + order: 7, + name: 'Nathan Smith', + runs: 280, + }, + { + order: 8, + name: 'Kyle Jamieson', + runs: 367, + }, + { + order: 9, + name: 'Matt Henry', + runs: 391, + }, + { + order: 10, + name: 'Glenn Phillips', + runs: 391, + }, + ], + byes: 22, + legByes: 20, + noBalls: 3, + penalties: 0, + wides: 8, + extras: 53, + }, + { + order: 2, + battingTeam: 'England', + runsScored: 119, + wickets: 2, + overs: '28.0', + declared: false, + forfeited: false, + description: 'England first innings', + batters: [ + { + name: 'Ben Duckett', + order: 1, + ballsFaced: 25, + runs: 36, + fours: 5, + sixes: 0, + out: true, + howOut: 'Run Out Smith', + onStrike: false, + nonStrike: true, + }, + { + name: 'Emilio Gay', + order: 2, + ballsFaced: 96, + runs: 48, + fours: 8, + sixes: 0, + out: false, + howOut: 'Not Out', + onStrike: false, + nonStrike: true, + }, + { + name: 'Jacob Bethell', + order: 3, + ballsFaced: 25, + runs: 9, + fours: 2, + sixes: 0, + out: true, + howOut: 'c Blundell b Smith', + onStrike: false, + nonStrike: true, + }, + { + name: 'Joe Root', + order: 4, + ballsFaced: 24, + runs: 20, + fours: 3, + sixes: 0, + out: false, + howOut: 'Not Out', + onStrike: true, + nonStrike: true, + }, + { + name: 'Harry Brook', + order: 5, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'James Rew', + order: 6, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Jordan Cox', + order: 7, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Jofra Archer', + order: 8, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Josh Tongue', + order: 9, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Matthew Fisher', + order: 10, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Sonny Baker', + order: 11, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + ], + bowlers: [ + { + name: 'Matt Henry', + order: 1, + overs: 9, + maidens: 2, + runs: 44, + wickets: 0, + balls: 54, + }, + { + name: 'Kyle Jamieson', + order: 2, + overs: 7, + maidens: 1, + runs: 38, + wickets: 0, + balls: 42, + }, + { + name: "Will O'Rourke", + order: 3, + overs: 7, + maidens: 2, + runs: 13, + wickets: 0, + balls: 42, + }, + { + name: 'Nathan Smith', + order: 4, + overs: 5, + maidens: 1, + runs: 20, + wickets: 1, + balls: 30, + }, + ], + fallOfWicket: [ + { + order: 1, + name: 'Ben Duckett', + runs: 45, + }, + { + order: 2, + name: 'Jacob Bethell', + runs: 68, + }, + ], + byes: 0, + legByes: 4, + noBalls: 2, + penalties: 0, + wides: 0, + extras: 6, + }, + ], + competitionName: 'Second Test Match', + stage: 'Second Test Match', + venueName: 'The Kia Oval', + result: 'in-play', + currentDay: 2, + totalDays: 5, + gameDate: '2026-06-17T10:00:00', + officials: [ + 'A T Holdstock', + 'N N Menon', + 'R J Tucker', + 'G D Lloyd', + 'A J Pycroft', + ], + matchId: '01553ce6-c76b-c050-b17b-c2654416b3ba', +}; + +export const resultMatch: FECricketMatch = { + ...liveMatch, + innings: [ + liveMatch.innings[0]!, + { + ...liveMatch.innings[1]!, + batters: [ + { + name: 'Ben Duckett', + order: 1, + ballsFaced: 25, + runs: 36, + fours: 5, + sixes: 0, + out: true, + howOut: 'Run Out Smith', + onStrike: false, + nonStrike: true, + }, + { + name: 'Emilio Gay', + order: 2, + ballsFaced: 96, + runs: 48, + fours: 8, + sixes: 0, + out: false, + howOut: 'Not Out', + onStrike: false, + nonStrike: true, + }, + { + name: 'Jacob Bethell', + order: 3, + ballsFaced: 25, + runs: 9, + fours: 2, + sixes: 0, + out: true, + howOut: 'c Blundell b Smith', + onStrike: false, + nonStrike: true, + }, + { + name: 'Joe Root', + order: 4, + ballsFaced: 24, + runs: 20, + fours: 3, + sixes: 0, + out: false, + howOut: 'Not Out', + onStrike: true, + nonStrike: true, + }, + + { + name: 'Harry Brook', + order: 5, + ballsFaced: 32, + runs: 24, + fours: 2, + sixes: 1, + out: true, + howOut: 'lbw b Henry', + onStrike: false, + nonStrike: true, + }, + { + name: 'James Rew', + order: 6, + ballsFaced: 27, + runs: 18, + fours: 3, + sixes: 0, + out: false, + howOut: 'Not Out', + onStrike: true, + nonStrike: true, + }, + { + name: 'Jordan Cox', + order: 7, + ballsFaced: 21, + runs: 11, + fours: 2, + sixes: 0, + out: false, + howOut: 'Not Out', + onStrike: false, + nonStrike: true, + }, + { + name: 'Jofra Archer', + order: 8, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Josh Tongue', + order: 9, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Matthew Fisher', + order: 10, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + { + name: 'Sonny Baker', + order: 11, + ballsFaced: 0, + runs: 0, + fours: 0, + sixes: 0, + out: false, + howOut: 'Yet to Bat', + onStrike: false, + nonStrike: false, + }, + ], + }, + ], + result: 'result', + currentDay: 5, + fullResult: { + resultType: 'home-win', + description: 'England win by 115 runs', + winner: { + winType: 'runs', + margin: '115', + team: 'England', + }, + }, +}; diff --git a/dotcom-rendering/fixtures/manual/footballData.ts b/dotcom-rendering/fixtures/manual/footballData.ts index 22723d05228..da7491b5b45 100644 --- a/dotcom-rendering/fixtures/manual/footballData.ts +++ b/dotcom-rendering/fixtures/manual/footballData.ts @@ -98,7 +98,7 @@ export const matchDayWorldCup: FootballMatches = [ score: 0, }, awayTeam: { - name: 'Bosnia-Herzegovina', + name: 'Bosnia and Herzegovina', id: '7531', score: 0, }, diff --git a/dotcom-rendering/fixtures/manual/highlights-trails.ts b/dotcom-rendering/fixtures/manual/highlights-trails.ts index b3d7464e93e..26158ee38ac 100644 --- a/dotcom-rendering/fixtures/manual/highlights-trails.ts +++ b/dotcom-rendering/fixtures/manual/highlights-trails.ts @@ -283,7 +283,7 @@ export const trails: Array = [ }, ]; -export const newsletterCard: DCRFrontCard = { +export const newsletterSignupCard: DCRFrontCard = { format: { theme: Pillar.News, design: ArticleDesign.Standard, @@ -307,6 +307,7 @@ export const newsletterCard: DCRFrontCard = { isBoosted: false, isCrossword: false, isNewsletter: true, + isNewsletterSignup: true, showQuotedHeadline: false, showLivePlayable: false, avatarUrl: undefined, diff --git a/dotcom-rendering/fixtures/manual/hostedArticle.ts b/dotcom-rendering/fixtures/manual/hostedArticle.ts index 0373d0b6000..795070bd7cf 100644 --- a/dotcom-rendering/fixtures/manual/hostedArticle.ts +++ b/dotcom-rendering/fixtures/manual/hostedArticle.ts @@ -1516,7 +1516,6 @@ export const hostedArticle: FEArticle = { shouldLoadGoogletag: true, inizio: true, }, - abTests: {}, serverSideABTests: { 'thefilter-at-a-glance-redesign-v2': 'stacked-default', }, diff --git a/dotcom-rendering/fixtures/manual/hostedGallery.ts b/dotcom-rendering/fixtures/manual/hostedGallery.ts index f20cf8b40d8..188f9472958 100644 --- a/dotcom-rendering/fixtures/manual/hostedGallery.ts +++ b/dotcom-rendering/fixtures/manual/hostedGallery.ts @@ -59,7 +59,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '1d9821eb-cfdc-4e01-bce5-0b9befa5c852', + elementId: '29edbdd8-bc98-4e3a-a2aa-fec7c85acb50', imageSources: [ { weighting: 'inline', @@ -302,7 +302,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: 'f805a4c4-5e71-4463-b086-b5c633de1acd', + elementId: 'be923392-d7a4-49a6-84d9-27fbea62351b', imageSources: [ { weighting: 'inline', @@ -581,7 +581,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '35286d25-810b-4bfb-9069-9c5b656c1ac1', + elementId: 'ffbbb89d-1125-469a-8b5a-45f7d620461b', imageSources: [ { weighting: 'inline', @@ -880,7 +880,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '6674e5f0-3995-40f2-9e09-c3b8e90d4da0', + elementId: '1d2c67c8-e6aa-4361-a792-6aab2229052a', imageSources: [ { weighting: 'inline', @@ -1169,7 +1169,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '0e07ff01-f58f-4883-84c9-26201dafa575', + elementId: '36c53367-f73f-4651-b95d-9e62f768237f', imageSources: [ { weighting: 'inline', @@ -1409,6 +1409,17 @@ export const hostedGallery: FEArticle = { credit: 'Photograph: Organizing for Action - Ohio/We Are Still In', }, }, + { + _type: 'model.dotcomrendering.pageElements.CallToActionAtomBlockElement', + trackingCode: 'WASI-1', + url: 'https://www.wearestillin.com/contribute', + image: 'https://media.guim.co.uk/ff5fc7b83674d49054ce29f138bb3e851835233e/0_307_4867_2921/4867.jpg', + label: 'America is still in. Are you?', + elementId: 'f8ce096e-e546-429a-a9ef-ebab6786e2d1', + title: 'We Are Still In (1st CTA)', + btnText: 'Commit to climate action', + id: 'fd0fc947-f32c-4d71-a59a-ebddd49c3167', + }, { displayCredit: true, _type: 'model.dotcomrendering.pageElements.ImageBlockElement', @@ -1478,7 +1489,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '4f944c60-c778-488d-a8a8-9056f48e878b', + elementId: 'd3b61d74-4bb3-4122-a38a-4443ea34bed8', imageSources: [ { weighting: 'inline', @@ -1788,7 +1799,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '3ad7442e-9f8d-42d8-8441-8b97ab1c0e65', + elementId: '4bf7619a-158d-4da1-ad75-3431982f7af5', imageSources: [ { weighting: 'inline', @@ -2098,7 +2109,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '891baf0e-4e31-452c-9218-1855db2c401c', + elementId: 'f3bd4a7f-7d02-4961-be73-fd376e352830', imageSources: [ { weighting: 'inline', @@ -2408,7 +2419,7 @@ export const hostedGallery: FEArticle = { }, ], }, - elementId: '2c437369-5e77-4e0a-9420-677a871b70c0', + elementId: '2101226a-5130-48ec-aa79-314f496c3bee', imageSources: [ { weighting: 'inline', @@ -2671,8 +2682,8 @@ export const hostedGallery: FEArticle = { byline: '', }, byline: '', - webPublicationDate: '2018-09-19T03:35:00.000+10:00', - webPublicationDateDeprecated: '2018-09-19T03:35:00.000+10:00', + webPublicationDate: '2018-09-18T17:35:00.000Z', + webPublicationDateDeprecated: '2018-09-18T17:35:00.000Z', webPublicationDateDisplay: 'Tue 18 Sep 2018 18.35 BST', webPublicationSecondaryDateDisplay: 'Last modified on Mon 6 Apr 2020 17.43 BST', @@ -2761,9 +2772,9 @@ export const hostedGallery: FEArticle = { name: 'Guardian staff reporter', }, ], - datePublished: '2018-09-19T03:35:00.000+10:00', + datePublished: '2018-09-18T17:35:00.000Z', headline: 'Faces of We Are Still In', - dateModified: '2020-04-07T02:43:11.000+10:00', + dateModified: '2020-04-06T16:43:11.000Z', mainEntityOfPage: 'https://www.theguardian.com/advertiser-content/we-are-still-in/faces-of-we-are-still-in', }, @@ -2788,14 +2799,14 @@ export const hostedGallery: FEArticle = { 'gnmguardian://advertiser-content/we-are-still-in/faces-of-we-are-still-in?contenttype=Article&source=applinks', 'article:publisher': 'https://www.facebook.com/theguardian', 'og:title': 'Faces of We Are Still In', - 'fb:app_id': '202314643182694', - 'article:modified_time': '2020-04-07T02:43:11.000+10:00', + 'fb:app_id': '180444840287', + 'article:modified_time': '2020-04-06T16:43:11.000Z', 'og:image:height': '420', 'og:description': 'A photo gallery', 'og:type': 'article', 'al:ios:app_store_id': '409128287', 'article:section': 'We Are Still In', - 'article:published_time': '2018-09-19T03:35:00.000+10:00', + 'article:published_time': '2018-09-18T17:35:00.000Z', 'article:tag': '', 'al:ios:app_name': 'The Guardian', 'og:site_name': 'the Guardian', @@ -2821,15 +2832,14 @@ export const hostedGallery: FEArticle = { config: { switches: { prebidCriteo: true, - externalVideoEmbeds: false, + externalVideoEmbeds: true, lightbox: true, - googleOneTapSwitch: false, - googleOneTap: false, + googleOneTapSwitch: true, hideNewsletterSignupComponentForSubscribers: true, prebidAppnexusUkRow: true, prebidMagnite: true, commercialMetrics: true, - prebidTrustx: true, + prebidTrustx: false, scAdFreeBanner: false, adaptiveSite: true, prebidPermutiveAudience: true, @@ -2837,82 +2847,82 @@ export const hostedGallery: FEArticle = { manyNewsletterVisibleRecaptcha: false, enableSentryReporting: true, lazyLoadContainers: true, - enableHlsWeb: false, - ampArticleSwitch: true, + filterAtAGlance: true, + ampArticleSwitch: false, remarketing: true, articleEndSlot: true, - keyEventsCarousel: false, - registerWithPhone: true, - darkModeWeb: true, + keyEventsCarousel: true, + registerWithPhone: false, targeting: true, remoteHeader: true, - ampPrebidOzone: true, + ampPrebidOzone: false, slotBodyEnd: true, emailInlineInFooter: true, showNewPrivacyWordingOnEmailSignupEmbeds: true, prebidAnalytics: true, extendedMostPopular: true, - ampContentAbTesting: true, + ampContentAbTesting: false, imrWorldwide: true, + prebidTeads: true, acast: true, twitterUwt: true, - abNoAuxiaSignInGate: false, prebidAppnexusInvcode: true, - ampPrebidPubmatic: true, + ampPrebidPubmatic: false, a9HeaderBidding: true, prebidAppnexus: true, enableDiscussionSwitch: true, prebidXaxis: true, - starRatingRedesign: false, stickyVideos: true, discussionAllPageSize: true, + showNewNewsletterSignupCard: true, prebidUserSync: true, - audioOnwardJourneySwitch: false, + audioOnwardJourneySwitch: true, brazeTaylorReport: false, callouts: true, sentinelLogger: true, geoMostPopular: true, weAreHiring: false, relatedContent: true, - thirdPartyEmbedTracking: false, + thirdPartyEmbedTracking: true, prebidOzone: true, - ampLiveblogSwitch: true, - ampAmazon: true, + ampLiveblogSwitch: false, + ampAmazon: false, mostViewedFronts: true, optOutAdvertising: true, googleSearch: true, brazeSwitch: true, - signInGate: false, + signInGate: true, prebidKargo: true, - disableChildDirected: false, - abAdmiralAdblockRecovery: true, consentManagement: true, - productLeftColCards: false, + disableChildDirected: true, + productLeftColCards: true, personaliseSignInGateAfterCheckout: true, idProfileNavigation: true, confiantAdVerification: true, discussionAllowAnonymousRecommendsSwitch: false, permutive: true, comscore: true, - ampPrebidCriteo: true, + ampPrebidCriteo: false, + prebidLiveramp: true, prebidTheTradeDesk: true, newsletterOnwards: false, youtubeIma: true, + usSignupHideMarketingToggle: true, webFonts: true, liveBlogTopSponsorship: true, lineItemJobs: true, ophan: true, - crosswordSvgThumbnails: false, + crosswordSvgThumbnails: true, prebidTriplelift: true, prebidPubmatic: true, - serverShareCounts: true, - autoRefresh: false, + serverShareCounts: false, + autoRefresh: true, enhanceTweets: true, prebidIndexExchange: true, prebidOpenx: true, prebidHeaderBidding: true, idCookieRefresh: true, - discussionPageSize: false, + discussionPageSize: true, smartAppBanner: false, historyTags: true, brazeContentCards: true, @@ -2922,60 +2932,175 @@ export const hostedGallery: FEArticle = { shouldLoadGoogletag: true, inizio: true, }, - abTests: {}, - serverSideABTests: {}, + serverSideABTests: { + 'commercial-hosted-gallery': 'preview', + }, googletagUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', - stage: 'DEV', - frontendAssetsFullURL: 'http://localhost:9000/assets/', + stage: 'PROD', + frontendAssetsFullURL: 'https://assets.guim.co.uk/', ampIframeUrl: - 'http://localhost:9000/assets/data/vendor/amp-iframe.html', + 'https://assets.guim.co.uk/data/vendor/2533d5cb94302889e6a8f1b24b5329e7/amp-iframe.html', + tones: 'Advertisement features', + avatarApiUrl: 'https://avatar.theguardian.com', + isSplash: false, + isColumn: false, + membershipUrl: 'https://membership.theguardian.com', headline: 'Faces of We Are Still In', + isImmersive: true, author: '', toneIds: 'tone/advertisement-features', ipsosTag: 'guardian', + isProd: true, + membershipAccess: '', + allowUserGeneratedContent: false, + commissioningDesks: '', webPublicationDate: 1537292100000, + forecastsapiurl: '/weatherapi/forecast', + supportUrl: 'https://support.theguardian.com', + isNumberedList: false, commercialBundleUrl: - 'https://assets.guim.co.uk/commercial/415d35be5f9cf7fdabe5/graun.standalone.commercial.js', + 'https://assets.guim.co.uk/commercial/d666613e3ada32e90aa0/graun.standalone.commercial.js', + idOAuthUrl: 'https://oauth.theguardian.com', + webTitle: 'Faces of We Are Still In', isPhotoEssay: false, + idWebAppUrl: 'https://oauth.theguardian.com', + a9PublisherId: '3722', + isFront: false, + inBodyInternalLinkCount: 0, + googleSearchUrl: '//www.google.co.uk/cse/cse.js', + inBodyExternalLinkCount: 0, showRelatedContent: false, + lightboxImages: { + id: 'advertiser-content/we-are-still-in/faces-of-we-are-still-in', + headline: 'Faces of We Are Still In', + shouldHideAdverts: false, + standfirst: '

A photo gallery

', + images: [ + { + caption: '', + credit: 'Photograph: We Are Still In', + displayCredit: true, + src: 'https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=700&quality=85&auto=format&fit=max&s=3d0b27fec6b0699ec64f76d2119ba306', + srcsets: + 'https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=1920&quality=85&auto=format&fit=max&s=f540242bfe5152256a4e9a97559cad2a 1920w, https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=1225&quality=85&auto=format&fit=max&s=5f48751ae7ee615948048dbd6c557f5a 1225w, https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=1065&quality=85&auto=format&fit=max&s=795d8fe51356b3c1b44886c7a7f9b08f 1065w, https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=965&quality=85&auto=format&fit=max&s=735200fccee556e92a22c2340f72895e 965w, https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=725&quality=85&auto=format&fit=max&s=6f24371c4d44fb6a5f9a3ec7b24b5106 725w, https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=645&quality=85&auto=format&fit=max&s=4f50af02f920f9df09dd8439849fe791 645w, https://i.guim.co.uk/img/media/3e10857a494a7765614b771e9f6d6ebfea41667b/0_79_802_481/master/802.jpg?width=465&quality=85&auto=format&fit=max&s=a1db2a209c898ef9727be73150191070 465w', + sizes: '(min-width: 1300px) 1920px, (min-width: 1140px) 1225px, (min-width: 980px) 1065px, (min-width: 740px) 965px, (min-width: 660px) 725px, (min-width: 480px) 645px, 465px', + ratio: 1.6673596673596673, + role: 'None', + parentContentId: + 'advertiser-content/we-are-still-in/faces-of-we-are-still-in', + id: 'advertiser-content/we-are-still-in/faces-of-we-are-still-in', + }, + ], + }, + googleSearchId: '007466294097402385199:m2ealvuxh1i', shouldHideReaderRevenue: true, - idUrl: 'https://profile.thegulocal.com', + idUrl: 'https://profile.theguardian.com', hasSurveyAd: false, - host: 'http://localhost:9000', - googleRecaptchaSiteKey: '6LcsAAodAAAAAC_HOsVfbpMF1tLcxyG1atxbtxIa', + omnitureAmpAccount: 'guardiangu-thirdpartyapps', + dfpAdUnitRoot: 'theguardian.com', + host: 'https://www.theguardian.com', + blogIds: '', + sectionName: 'We Are Still In', + hasMultipleVideosInPage: false, + hasShowcaseMainElement: false, + googleRecaptchaSiteKey: '6Le7mFsrAAAAAD07aRCTHZe9u0EfvOiNe8Y0DMMV', + fbAppId: '180444840287', + isContent: true, shortUrlId: '/p/9c737', dfpAccountId: '59666047', + plistaPublicApiKey: '462925f4f131001fd974bebe', + wordCount: 176, hasLiveBlogTopAd: false, dcrSentryDsn: 'https://1937ab71c8804b2b8438178dfdd6468f@sentry.io/1377847', + cardStyle: 'media', adUnit: '/59666047/theguardian.com/advertiser-content/we-are-still-in/article/ng', - discussionApiUrl: - 'https://discussion.code.dev-theguardian.com/discussion-api', + discussionApiUrl: 'https://discussion.theguardian.com/discussion-api', isSensitive: false, + ophanEmbedJsUrl: '//j.ophan.co.uk/ophan.embed', edition: 'UK', - brazeApiKey: '9d722c47-c889-49e7-b5d6-17e0a5956185', - discussionApiClientHeader: 'nextgen-dev', + frontendSentryDsn: + 'https://344003a8d11c41d8800fbad8383fdc50@o14302.ingest.us.sentry.io/35463', + brazeApiKey: '7f28c639-8bda-48ff-a3f6-24345abfc07c', + blogs: '', + discussionApiClientHeader: 'nextgen', section: 'advertiser-content/we-are-still-in', + userAttributesApiUrl: + 'https://members-data-api.theguardian.com/user-attributes', videoDuration: 0, + disableStickyTopBanner: true, + dfpHost: 'pubads.g.doubleclick.net', + weatherapiurl: '/weatherapi/city', sentryPublicApiKey: '344003a8d11c41d8800fbad8383fdc50', + shortUrl: 'https://www.theguardian.com/p/9c737', + pillar: '', pageId: 'advertiser-content/we-are-still-in/faces-of-we-are-still-in', references: [], + beaconUrl: '//phar.gu-web.net', + commentable: false, discussionD2Uid: 'zHoBy6HNKsk', + ophanJsUrl: '//j.ophan.co.uk/ophan.ng', + contributorBio: '', keywordIds: '', googleRecaptchaSiteKeyVisible: - '6LfZin8rAAAAAAvMlIx_dupDQKcBJ9hQVLclYd09', + '6LcTl8srAAAAAKwyUhd4nJHHe2-hCrPKcWnawY0F', contentType: 'Article', - isDev: true, + isDev: false, + isHosted: true, + facebookIaAdUnitRoot: 'facebook-instant-articles', + sponsorshipType: 'paid-content', + isAdFree: false, + stripePublicToken: 'pk_live_2O6zPMHXNs2AGea4bAmq5R7Z', + omnitureAccount: 'guardiangu-network', + locationapiurl: '/weatherapi/locations?query=', + authorIds: '', isPaidContent: true, - ajaxUrl: 'http://localhost:9000', + hasYouTubeAtom: false, + externalEmbedHost: 'https://embed.theguardian.com', + thirdPartyAppsAccount: 'guardiangu-thirdpartyapps', + ajaxUrl: 'https://api.nextgen.guardianapps.co.uk', + byline: '', keywords: '', - revisionNumber: 'a0f26041f01f9df1cc879cd484aff1837f998895', + contentId: + 'advertiser-content/we-are-still-in/faces-of-we-are-still-in', + nonKeywordTagIds: + 'advertiser-content/we-are-still-in/we-are-still-in,type/gallery,tone/advertisement-features', + mobileAppsAdUnitRoot: 'beta-guardian-app', + hasPageSkin: false, + requiresMembershipAccess: false, + revisionNumber: 'DEV', + optimizeEpicUrl: + 'https://support.theguardian.com/epic/control/index.html', + assetsPath: 'https://assets.guim.co.uk/', mmaUrl: 'https://manage.theguardian.com', isLive: false, + richLink: '', + campaigns: [], isLiveBlog: false, + pageCode: '5024356', + avatarImagesUrl: 'https://avatar.guim.co.uk', + publication: 'theguardian.com', sentryHost: 'app.getsentry.com/35463', + buildNumber: 'DEV', + atomTypes: { + review: false, + guide: false, + audio: false, + explainer: false, + interactive: false, + profile: false, + chart: false, + quizz: false, + callToAction: true, + commonsdivision: false, + timeline: false, + media: false, + qanda: false, + }, + userBenefitsApiUrl: + 'https://user-benefits.guardianapis.com/benefits/me', sharedAdTargeting: { - ct: 'gallery', + sens: 'f', url: '/advertiser-content/we-are-still-in/faces-of-we-are-still-in', br: 'p', su: ['0'], @@ -2983,11 +3108,35 @@ export const hostedGallery: FEArticle = { tn: ['advertisement-features'], p: 'ng', sh: 'https://www.theguardian.com/p/9c737', + ct: 'gallery', + s: 'advertiser-content/we-are-still-in', }, - idApiUrl: 'https://id.code.dev-guardianapis.com', + onwardWebSocket: + 'ws://api.nextgen.guardianapps.co.uk/recently-published', + productionOffice: 'Us', + shouldHideAdverts: false, + idApiUrl: 'https://idapi.theguardian.com', + pbIndexSites: [ + { + bp: 'D', + id: 208283, + }, + { + bp: 'M', + id: 213553, + }, + { + bp: 'T', + id: 215488, + }, + ], + googletagJsUrl: '//securepubads.g.doubleclick.net/tag/js/gpt.js', + atoms: ['fd0fc947-f32c-4d71-a59a-ebddd49c3167'], + calloutsUrl: + 'https://callouts.guardianapis.com/formstack-campaign/submit', isPreview: false, }, - guardianBaseURL: 'http://localhost:9000', + guardianBaseURL: 'https://www.theguardian.com', contentType: 'Article', hasRelated: false, hasStoryPackage: false, @@ -3032,8 +3181,8 @@ export const hostedGallery: FEArticle = { value: ['0'], }, { - name: 'br', - value: 'p', + name: 's', + value: 'advertiser-content/we-are-still-in', }, { name: 'edition', @@ -3047,6 +3196,14 @@ export const hostedGallery: FEArticle = { name: 'p', value: 'ng', }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, { name: 'url', value: '/advertiser-content/we-are-still-in/faces-of-we-are-still-in', @@ -3094,21 +3251,29 @@ export const hostedGallery: FEArticle = { name: 'su', value: ['0'], }, + { + name: 'tn', + value: ['advertisement-features'], + }, + { + name: 'p', + value: 'ng', + }, { name: 'br', value: 'p', }, { - name: 'edition', - value: 'au', + name: 'sens', + value: 'f', }, { - name: 'tn', - value: ['advertisement-features'], + name: 's', + value: 'advertiser-content/we-are-still-in', }, { - name: 'p', - value: 'ng', + name: 'edition', + value: 'au', }, { name: 'url', @@ -3158,8 +3323,8 @@ export const hostedGallery: FEArticle = { value: ['0'], }, { - name: 'br', - value: 'p', + name: 's', + value: 'advertiser-content/we-are-still-in', }, { name: 'edition', @@ -3173,6 +3338,14 @@ export const hostedGallery: FEArticle = { name: 'p', value: 'ng', }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, { name: 'url', value: '/advertiser-content/we-are-still-in/faces-of-we-are-still-in', @@ -3221,8 +3394,8 @@ export const hostedGallery: FEArticle = { value: ['0'], }, { - name: 'br', - value: 'p', + name: 's', + value: 'advertiser-content/we-are-still-in', }, { name: 'tn', @@ -3236,6 +3409,14 @@ export const hostedGallery: FEArticle = { name: 'p', value: 'ng', }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, { name: 'url', value: '/advertiser-content/we-are-still-in/faces-of-we-are-still-in', @@ -3284,8 +3465,8 @@ export const hostedGallery: FEArticle = { value: ['0'], }, { - name: 'br', - value: 'p', + name: 's', + value: 'advertiser-content/we-are-still-in', }, { name: 'edition', @@ -3299,6 +3480,14 @@ export const hostedGallery: FEArticle = { name: 'p', value: 'ng', }, + { + name: 'br', + value: 'p', + }, + { + name: 'sens', + value: 'f', + }, { name: 'url', value: '/advertiser-content/we-are-still-in/faces-of-we-are-still-in', @@ -3386,6 +3575,10 @@ export const hostedGallery: FEArticle = { }, ], }, + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'US politics', url: '/us-news/us-politics', @@ -3451,6 +3644,10 @@ export const hostedGallery: FEArticle = { title: 'Football', url: '/football', children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'Live scores', url: '/football/live', @@ -3599,7 +3796,7 @@ export const hostedGallery: FEArticle = { }, { title: 'Columnists', - url: '/index/contributors', + url: '/uk/columnists', }, { title: 'Cartoons', @@ -3621,10 +3818,18 @@ export const hostedGallery: FEArticle = { longTitle: 'Sport home', iconName: 'home', children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'Football', url: '/football', children: [ + { + title: 'World Cup 2026', + url: '/football/world-cup-2026', + }, { title: 'Live scores', url: '/football/live', @@ -4223,7 +4428,7 @@ export const hostedGallery: FEArticle = { publication: 'theguardian.com', shouldHideReaderRevenue: true, slotMachineFlags: '', - contributionsServiceUrl: 'https://contributions.code.dev-guardianapis.com', + contributionsServiceUrl: 'https://contributions.guardianapis.com', isSpecialReport: false, showTableOfContents: false, lang: 'en', diff --git a/dotcom-rendering/fixtures/manual/hostedVideo.ts b/dotcom-rendering/fixtures/manual/hostedVideo.ts index b7f9911637a..4c61eb6e395 100644 --- a/dotcom-rendering/fixtures/manual/hostedVideo.ts +++ b/dotcom-rendering/fixtures/manual/hostedVideo.ts @@ -326,7 +326,6 @@ export const hostedVideo: FEArticle = { shouldLoadGoogletag: true, inizio: true, }, - abTests: {}, serverSideABTests: { 'thefilter-at-a-glance-redesign-v2': 'stacked-default', }, diff --git a/dotcom-rendering/fixtures/manual/productBlockElement.ts b/dotcom-rendering/fixtures/manual/productBlockElement.ts index 54d96702139..3f15a360496 100644 --- a/dotcom-rendering/fixtures/manual/productBlockElement.ts +++ b/dotcom-rendering/fixtures/manual/productBlockElement.ts @@ -214,12 +214,14 @@ export const exampleProduct: ProductBlockElement = { _type: 'model.dotcomrendering.pageElements.LinkBlockElement', url: 'https://www.johnlewis.com/bosch-twk7203gb-sky-variable-temperature-kettle-1-7l-black/p3228625', label: '£79.99 at John Lewis', + elementId: '123', linkType: 'ProductButton', }, { _type: 'model.dotcomrendering.pageElements.LinkBlockElement', url: 'https://www.amazon.co.uk/Bosch-TWK7203GB-Sky-Variable-Temperature/dp/B07Z8VQ2V6', label: '£79.99 at Amazon', + elementId: '1234', linkType: 'ProductButton', }, { diff --git a/dotcom-rendering/fixtures/manual/trails.ts b/dotcom-rendering/fixtures/manual/trails.ts index 5050025eae3..0637a3435d9 100644 --- a/dotcom-rendering/fixtures/manual/trails.ts +++ b/dotcom-rendering/fixtures/manual/trails.ts @@ -759,7 +759,7 @@ export const selfHostedLoopVideo53Card = { ], aspectRatio: 5 / 3, image: { - ...selfHostedLoopVideo54Card.mainMedia.image, + src: 'https://media.guim.co.uk/211404dc9a4a7fa10fcf6a2f08e930327b2f7f3c/0_184_2909_1637/2000.jpg', aspectRatio: '5:3', }, }, @@ -773,7 +773,7 @@ export const selfHostedLoopVideo916Card = { sources: [ { mimeType: 'video/mp4', - src: 'https://uploads.guimcode.co.uk/2025/11/12/5x4_test--ee49513c-bf16-4321-a444-09c9a037d584-4.0.mp4', + src: 'https://uploads.guim.co.uk/2026/06/05/Is_it_possible_to_walk_to_MetLife_Stadium_from_New_York_City_--e4247040-30a8-4bd4-990b-9b80f0e8616e-2.0_720h.mp4"', width: 406, height: 720, hasAudio: true, @@ -781,7 +781,7 @@ export const selfHostedLoopVideo916Card = { ], aspectRatio: 9 / 16, image: { - ...selfHostedLoopVideo54Card.mainMedia.image, + src: 'https://uploads.guim.co.uk/2026/06/05/Is_it_possible_to_walk_to_MetLife_Stadium_from_New_York_City_--e4247040-30a8-4bd4-990b-9b80f0e8616e-2.0.0000000.jpg', aspectRatio: '9:16', }, }, @@ -791,11 +791,14 @@ export const selfHostedLoopVideo169Card = { ...selfHostedLoopVideo54Card, headline: 'Self-hosted 16:9 loop video card', mainMedia: { - ...selfHostedLoopVideo54Card.mainMedia, + type: 'SelfHostedVideo', + videoStyle: 'Loop', + atomId: '3cb22b60-2c3f-48d6-8bce-38c956907cce', + duration: 30, sources: [ { mimeType: 'video/mp4', - src: 'https://uploads.guim.co.uk/2026/01/02/Social_media_footage_shows_person_trying_to_put_out_flames_in_Crans-Montana_bar___video--77ec00d2-7e58-4698-898c-08174f65a94b-1.0.mp4', + src: 'https://uploads.guim.co.uk/2026/06/24/Samsung_Galaxy_S26_Series_Main_Media--652aad98-a2ce-4cc5-98d8-f67eafea4819-1.0_720h.mp4', width: 1280, height: 720, hasAudio: true, @@ -803,7 +806,7 @@ export const selfHostedLoopVideo169Card = { ], aspectRatio: 16 / 9, image: { - ...selfHostedLoopVideo54Card.mainMedia.image, + src: 'https://uploads.guim.co.uk/2026/06/24/Samsung_Galaxy_S26_Series_Main_Media--652aad98-a2ce-4cc5-98d8-f67eafea4819-1.0.0000000.jpg', aspectRatio: '16:9', }, }, diff --git a/dotcom-rendering/jest.config.js b/dotcom-rendering/jest.config.js index e282699b5c1..5257a547f9c 100644 --- a/dotcom-rendering/jest.config.js +++ b/dotcom-rendering/jest.config.js @@ -2,19 +2,25 @@ const swcConfig = require('./webpack/.swcrc.json'); const esModules = [ '@guardian/', + '@csstools', + '@exodus', + '@asamuzakjp', + '@bramus', 'screenfull', 'node-fetch', 'data-uri-to-buffer', 'fetch-blob', 'formdata-polyfill', 'storybook', + 'parse5', + 'entities', ].join('|'); module.exports = { testEnvironment: 'jsdom', moduleDirectories: ['node_modules', 'src'], transform: { - '^.+\\.(js|ts|tsx)$': ['@swc/jest', swcConfig], + '^.+\\.(mjs|js|ts|tsx)$': ['@swc/jest', swcConfig], }, testMatch: ['**/*.test.+(ts|tsx|js)'], setupFilesAfterEnv: ['/scripts/jest/setup.ts'], diff --git a/dotcom-rendering/package.json b/dotcom-rendering/package.json index e69039cba6e..480a8b057a1 100644 --- a/dotcom-rendering/package.json +++ b/dotcom-rendering/package.json @@ -28,19 +28,19 @@ "@emotion/react": "11.14.0", "@emotion/server": "11.11.0", "@guardian/ab-testing-config": "workspace:ab-testing-config", - "@guardian/braze-components": "22.2.0", + "@guardian/braze-components": "23.0.2", "@guardian/bridget": "8.13.1", "@guardian/browserslist-config": "6.1.0", "@guardian/cdk": "catalog:", - "@guardian/consent-manager": "1.0.0", "@guardian/commercial-core": "34.0.0", + "@guardian/consent-manager": "1.0.0", "@guardian/core-web-vitals": "7.0.0", "@guardian/eslint-config": "catalog:", "@guardian/identity-auth": "6.0.1", "@guardian/identity-auth-frontend": "8.1.0", "@guardian/libs": "32.0.0", - "@guardian/ophan-tracker-js": "4.0.1", - "@guardian/react-crossword": "17.1.0", + "@guardian/ophan-tracker-js": "4.0.2", + "@guardian/react-crossword": "19.0.1", "@guardian/shimport": "1.0.2", "@guardian/source": "12.2.1", "@guardian/source-development-kitchen": "28.1.0", @@ -53,9 +53,10 @@ "@storybook/addon-webpack5-compiler-swc": "4.0.3", "@storybook/react-webpack5": "10.3.3", "@svgr/webpack": "8.1.0", - "@swc/cli": "0.7.8", - "@swc/core": "1.13.5", + "@swc/cli": "0.8.1", + "@swc/core": "1.15.41", "@swc/jest": "0.2.39", + "@swc/plugin-emotion": "14.12.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.0", @@ -124,7 +125,7 @@ "is-mobile": "3.1.1", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jsdom": "25.0.1", + "jsdom": "29.1.1", "lodash.debounce": "4.0.8", "log4js": "6.9.1", "lz-string": "1.5.0", diff --git a/dotcom-rendering/playwright/fixtures/cricket-match.js b/dotcom-rendering/playwright/fixtures/cricket-match.js index 2d93dd1b4ae..254061e6bee 100644 --- a/dotcom-rendering/playwright/fixtures/cricket-match.js +++ b/dotcom-rendering/playwright/fixtures/cricket-match.js @@ -140,7 +140,8 @@ export const match1 = { fallOfWicket: [], }, ], - competitionName: 'Third Test Match', + competitionName: 'Test Match Series', + stage: 'Third Test Match', venueName: 'National Cricket Stadium', gameDate: '2022-03-24T14:00:00', matchId: '0fddabb8-a5ce-eef6-bfbd-e80d1edc7805', @@ -298,7 +299,8 @@ export const match2 = { ], }, ], - competitionName: 'Third Test Match', + competitionName: 'Test Match Series', + stage: 'Third Test Match', venueName: 'National Cricket Stadium', gameDate: '2022-03-24T14:00:00', matchId: '0fddabb8-a5ce-eef6-bfbd-e80d1edc7805', diff --git a/dotcom-rendering/playwright/tests/article.embeds.e2e.spec.ts b/dotcom-rendering/playwright/tests/article.embeds.e2e.spec.ts index 7b19fcbdc5c..6f136ddafb6 100644 --- a/dotcom-rendering/playwright/tests/article.embeds.e2e.spec.ts +++ b/dotcom-rendering/playwright/tests/article.embeds.e2e.spec.ts @@ -7,7 +7,7 @@ import { expectToBeVisible } from '../lib/locators'; test.describe('Embeds', () => { test.describe('WEB', function () { - test.skip('should render the click to view overlay revealing the embed when clicked', async ({ + test('should render the click to view overlay revealing the embed when clicked', async ({ page, }) => { await loadPage({ @@ -26,12 +26,12 @@ test.describe('Embeds', () => { .locator('button[data-testid="click-to-view-button"]') .click(); - await expect( - await getIframeBody( - page, - 'div[data-testid="embed-block"] > div > iframe', - ), - ).toContainText('Radiolab'); + const iframeBodyLocator = await getIframeBody( + page, + 'div[data-testid="embed-block"] > div > iframe', + ); + + await expect(iframeBodyLocator.locator('audio')).toBeAttached(); }); test('should render the interactive 1', async ({ page }) => { diff --git a/dotcom-rendering/scripts/jest/setup.ts b/dotcom-rendering/scripts/jest/setup.ts index 452ba1227a2..ecac42260c7 100644 --- a/dotcom-rendering/scripts/jest/setup.ts +++ b/dotcom-rendering/scripts/jest/setup.ts @@ -1,6 +1,8 @@ // add some helpful assertions import '@testing-library/jest-dom'; +import { ReadableStream } from 'node:stream/web'; import { TextDecoder, TextEncoder } from 'node:util'; +import { MessagePort } from 'node:worker_threads'; import { isServer } from '../../src/lib/isServer'; import type { Guardian } from '../../src/model/guardian'; @@ -15,7 +17,6 @@ const windowGuardianConfig = { browserId: 'jest-browser-id', pageViewId: 'jest-page-view-id', }, - tests: {}, switches: {}, } as Guardian['config']; @@ -108,6 +109,26 @@ if (!isServer) { */ global.TextEncoder = TextEncoder as unknown as typeof global.TextEncoder; global.TextDecoder = TextDecoder as unknown as typeof global.TextDecoder; +global.ReadableStream = + ReadableStream as unknown as typeof global.ReadableStream; +global.MessagePort = MessagePort as unknown as typeof global.MessagePort; + +if (!isServer) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: string): MediaQueryList => + ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }) as MediaQueryList, + }); +} // Mocks the version number used by CDK, we don't want our tests to fail every time we update our cdk dependency. jest.mock('@guardian/cdk/lib/constants/tracking-tag'); diff --git a/dotcom-rendering/scripts/test-data/gen-fixtures.js b/dotcom-rendering/scripts/test-data/gen-fixtures.js index b339f5e320f..095f6d55a1a 100644 --- a/dotcom-rendering/scripts/test-data/gen-fixtures.js +++ b/dotcom-rendering/scripts/test-data/gen-fixtures.js @@ -131,6 +131,14 @@ const articles = [ name: 'Video', url: 'https://www.theguardian.com/sport/video/2023/nov/20/atp-finals-djokovic-beats-sinner-to-claim-record-seventh-title-video', }, + { + name: 'AffiliateProductShowcase', + url: 'https://www.theguardian.com/thefilter/2026/may/04/things-you-loved-most-april-2026', + }, + { + name: 'AffiliateProductStandard', + url: 'https://www.theguardian.com/thefilter-us/2025/dec/25/glass-food-containers-to-store-leftovers', + }, ]; const HEADER = `/** @@ -360,7 +368,7 @@ requests.push( requests.push( // This match data will expire after two months, find a new match if this needs updating fetch( - 'https://www.theguardian.com/sport/cricket/match/2025-08-04/england-cricket-team.json?dcr', + 'https://www.theguardian.com/sport/cricket/match/2026-06-04/england-cricket-team.json?dcr', ) .then((res) => res.json()) .then((json) => { diff --git a/dotcom-rendering/src/client/adaptiveSite.ts b/dotcom-rendering/src/client/adaptiveSite.ts index 6f86e9657a5..307fb52a07d 100644 --- a/dotcom-rendering/src/client/adaptiveSite.ts +++ b/dotcom-rendering/src/client/adaptiveSite.ts @@ -16,19 +16,6 @@ export const shouldAdapt = async (): Promise => { if (!window.guardian.config.switches.adaptiveSite) return false; if (window.location.host !== 'www.theguardian.com') return false; - /** - * The europe beta front is being served to a 0% audience. This means it's rarely in cache and so it gets adapted more often. - * This is a temporary measure to ensure that the front is not adapted during testing. - */ - if ( - (window.guardian.config.tests.europeBetaFrontVariant === 'variant' || - window.guardian.config.tests.europeBetaFrontTest2Variant === - 'variant') && - window.location.pathname === '/europe' - ) { - return false; - } - // only evaluate this code if we want to adapt in response to page performance const { isPerformingPoorly } = await import( /* webpackMode: "eager" */ './poorPerformanceMonitoring' diff --git a/dotcom-rendering/src/client/bootCmp.ts b/dotcom-rendering/src/client/bootCmp.ts index 5bdc46ba88d..62410b5a985 100644 --- a/dotcom-rendering/src/client/bootCmp.ts +++ b/dotcom-rendering/src/client/bootCmp.ts @@ -58,8 +58,9 @@ export const bootCmp = async ( ): Promise => { if (!window.guardian.config.switches.consentManagement) return; // CMP turned off! + // Initalise CMP before attempting to submit consent to Ophan, otherwise the consent state may be empty on first load. + await initialiseCmp(); await Promise.all([ - initialiseCmp(), eagerlyImportPrivacySettingsLinkIsland(), submitConsentToOphan(renderingTarget), ]); diff --git a/dotcom-rendering/src/client/sentryLoader/sentry.ts b/dotcom-rendering/src/client/sentryLoader/sentry.ts index 6a984e04801..ed8122b14b9 100644 --- a/dotcom-rendering/src/client/sentryLoader/sentry.ts +++ b/dotcom-rendering/src/client/sentryLoader/sentry.ts @@ -48,10 +48,7 @@ Sentry.init({ // sampleRate: // We use Math.random in init.ts to sample errors }); -if ( - BUILD_VARIANT && - window.guardian.config.tests[dcrJavascriptBundle('Variant')] === 'variant' -) { +if (BUILD_VARIANT) { Sentry.setTag('dcr.bundle', dcrJavascriptBundle('Variant')); } diff --git a/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts b/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts index 23f7b48af9b..43b2845d65e 100644 --- a/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts +++ b/dotcom-rendering/src/client/sentryLoader/sentryLoader.ts @@ -1,4 +1,4 @@ -import { BUILD_VARIANT, dcrJavascriptBundle } from '../../../webpack/bundles'; +import { BUILD_VARIANT } from '../../../webpack/bundles'; import { loadSentryOnError } from './loadSentry'; type IsSentryEnabled = { @@ -46,10 +46,9 @@ const stubSentry = (): void => { }; export const sentryLoader = (): Promise => { - const { switches, isDev, tests } = window.guardian.config; + const { switches, isDev } = window.guardian.config; const enableSentryReporting = !!switches.enableSentryReporting; - const isInBrowserVariantTest = - BUILD_VARIANT && tests[dcrJavascriptBundle('Variant')] === 'variant'; + const isInBrowserVariantTest = BUILD_VARIANT; const canLoadSentry = isSentryEnabled({ enableSentryReporting, diff --git a/dotcom-rendering/src/client/userFeatures/user-features.test.ts b/dotcom-rendering/src/client/userFeatures/user-features.test.ts index 140cb78e020..1ec49697c82 100644 --- a/dotcom-rendering/src/client/userFeatures/user-features.test.ts +++ b/dotcom-rendering/src/client/userFeatures/user-features.test.ts @@ -67,7 +67,6 @@ const setAllBenefitsData = (opts: { isExpired: boolean }) => { beforeAll(() => { window.guardian.config.page.userBenefitsApiUrl = 'fake-url'; - window.guardian.config.tests['useUserBenefitsApiVariant'] = 'variant'; }); const expectUserBenefitExpiryCookieHasBeenSetCorrectly = () => { diff --git a/dotcom-rendering/src/components/AdSlot.web.tsx b/dotcom-rendering/src/components/AdSlot.web.tsx index 3d11ac25d99..6b92ff4bc20 100644 --- a/dotcom-rendering/src/components/AdSlot.web.tsx +++ b/dotcom-rendering/src/components/AdSlot.web.tsx @@ -10,7 +10,6 @@ import { until, } from '@guardian/source/foundations'; import { Hide } from '@guardian/source/react-components'; -import type { FEArticle } from '../frontend/feArticle'; import { labelBoxStyles, labelHeight, labelStyles } from '../lib/adStyles'; import { ArticleDisplay } from '../lib/articleFormat'; import { center as layoutCenterStyles } from '../lib/center'; @@ -352,10 +351,12 @@ const liveBlogTopContainerStyles = css` const mobileStickyAdStyles = css` position: fixed; bottom: 0; - width: 320px; + width: 100%; margin: 0 auto; right: 0; left: 0; + text-align: center; + background-color: ${schemedPalette('--ad-background')}; z-index: ${getZIndex('mobileSticky')}; ${from.phablet} { display: none; @@ -401,18 +402,9 @@ const mobileStickyAdStyles = css` content: 'Advertisement'; display: block; position: relative; - ${labelBoxStyles} - } -`; - -const mobileStickyAdStylesFullWidth = css` - width: 100%; - text-align: center; - background-color: ${palette.neutral[97]}; - - .ad-slot[data-label-show='true']::before { padding-left: calc((100% - ${adSizes.mobilesticky.width}px) / 2); padding-right: calc((100% - ${adSizes.mobilesticky.width}px) / 2); + ${labelBoxStyles} } `; @@ -990,22 +982,8 @@ export const AdSlot = ({ } }; -type MobileStickyContainerProps = Pick; - -export const MobileStickyContainer = ({ - contentType, - pageId, -}: MobileStickyContainerProps) => { +export const MobileStickyContainer = () => { return ( -
+
); }; diff --git a/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx b/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx index 63308602440..be31944a620 100644 --- a/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx +++ b/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx @@ -59,7 +59,6 @@ export const AllEditorialNewslettersPage = ({ commercialMetricsEnabled={ !!newslettersPage.config.switches.commercialMetrics } - tests={newslettersPage.config.abTests} /> ); } + case ArticleDesign.HostedGallery: { + return ( +
+

+ + {headlineString} + +

+
+ ); + } default: return (
diff --git a/dotcom-rendering/src/components/ArticlePage.tsx b/dotcom-rendering/src/components/ArticlePage.tsx index 12a98d76e29..a5b9d8eaf06 100644 --- a/dotcom-rendering/src/components/ArticlePage.tsx +++ b/dotcom-rendering/src/components/ArticlePage.tsx @@ -113,7 +113,6 @@ export const ArticlePage = (props: WebProps | AppProps) => { commercialMetricsEnabled={ !!frontendData.config.switches.commercialMetrics } - tests={frontendData.config.abTests} /> @@ -136,10 +135,7 @@ export const ArticlePage = (props: WebProps | AppProps) => { } /> - {isGoogleOneTapEnabled( - frontendData.config.abTests, - frontendData.config.switches, - ) && ( + {isGoogleOneTapEnabled(frontendData.config.switches) && ( (
diff --git a/dotcom-rendering/src/components/BackToTop.tsx b/dotcom-rendering/src/components/BackToTop.tsx index 5b1eb65e8de..efad4884e98 100644 --- a/dotcom-rendering/src/components/BackToTop.tsx +++ b/dotcom-rendering/src/components/BackToTop.tsx @@ -2,9 +2,12 @@ import { css } from '@emotion/react'; import { brandBackground, brandText, + calculateHoverColour, palette, + palette as sourcePalette, textSansBold15, } from '@guardian/source/foundations'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; const iconHeight = '42px'; @@ -33,6 +36,18 @@ const link = css` } `; +const hostedGalleryLink = css` + :hover { + color: ${calculateHoverColour(sourcePalette.neutral[100])}; + + .icon-container { + background-color: ${calculateHoverColour( + sourcePalette.neutral[100], + )}; + } + } +`; + const icon = css` ::before { position: absolute; @@ -56,11 +71,18 @@ const textStyles = css` padding-right: 5px; `; -export const BackToTop = () => ( - - Back to top - - - - -); +export const BackToTop = ({ format }: { format?: ArticleFormat }) => { + const isHostedGallery = format?.design === ArticleDesign.HostedGallery; + return ( + + Back to top + + + + + ); +}; diff --git a/dotcom-rendering/src/components/Card/Card.tsx b/dotcom-rendering/src/components/Card/Card.tsx index aadde807692..26465fde401 100644 --- a/dotcom-rendering/src/components/Card/Card.tsx +++ b/dotcom-rendering/src/components/Card/Card.tsx @@ -164,6 +164,7 @@ export type Props = { headlinePosition?: 'inner' | 'outer'; starRatingSize?: RatingSizeType; contentSpacing?: 'small' | 'large'; + allowHeadlineToBreakWords?: boolean; }; const waveformWrapper = ( @@ -410,6 +411,7 @@ export const Card = ({ starRatingSize = 'small', articleMedia, contentSpacing, + allowHeadlineToBreakWords, }: Props) => { const ab = useAB(); const isInLoopClickTestControl = Boolean( @@ -600,9 +602,7 @@ export const Card = ({ return 'tablet'; }; - const shouldShowTrailText = isMoreGalleriesOnwardContent - ? media?.type !== 'podcast' && isOnwardSplash - : media?.type !== 'podcast'; + const shouldShowTrailText = media?.type !== 'podcast'; /** * Determines the gap of between card components based on card properties @@ -823,6 +823,7 @@ export const Card = ({ ? palette('--onward-content-top-border') : undefined } + isLoopAndInLoopClickTestVariant={isLoopAndInLoopClickTestVariant} > {!isUndefined(starRating) && ( @@ -895,6 +897,9 @@ export const Card = ({ mediaPositionOnMobile={mediaPositionOnMobile} padMedia={isMediaCardOrNewsletter && !isOnwardsContent} isSmallCard={isSmallCard} + isLoopAndInLoopClickTestVariant={ + isLoopAndInLoopClickTestVariant + } > {media.type === 'slideshow' && ( {!isUndefined(starRating) && ( diff --git a/dotcom-rendering/src/components/Card/components/CardWrapper.tsx b/dotcom-rendering/src/components/Card/components/CardWrapper.tsx index 0a0ce1e0814..3c98f802988 100644 --- a/dotcom-rendering/src/components/Card/components/CardWrapper.tsx +++ b/dotcom-rendering/src/components/Card/components/CardWrapper.tsx @@ -13,6 +13,7 @@ type Props = { showTopBarMobile: boolean; containerPalette?: DCRContainerPalette; topBarColour?: string; + isLoopAndInLoopClickTestVariant?: boolean; }; const baseCardStyles = css` @@ -41,7 +42,7 @@ const baseCardStyles = css` text-decoration: none; `; -const hoverStyles = css` +const hoverStyles = (isLoopAndInLoopClickTestVariant: boolean) => css` :hover .media-overlay { width: 100%; height: 100%; @@ -59,7 +60,11 @@ const hoverStyles = css` */ :has( ul.sublinks:hover, - .video-container:not(.cinemagraph):hover, + .video-container:not( + ${isLoopAndInLoopClickTestVariant + ? `.cinemagraph, .loop` + : `.cinemagraph`} + ):hover, .slideshow-carousel-footer:hover, .branding-logo:hover ) { @@ -100,6 +105,7 @@ export const CardWrapper = ({ showTopBarMobile, containerPalette, topBarColour = palette('--card-border-top'), + isLoopAndInLoopClickTestVariant, }: Props) => { return ( @@ -107,7 +113,7 @@ export const CardWrapper = ({
{ const isHorizontalOnMobile = mediaPositionOnMobile === 'left' || mediaPositionOnMobile === 'right'; @@ -209,6 +215,7 @@ export const MediaWrapper = ({ css` /* position relative is required here to bound the image overlay */ position: relative; + img { width: 100%; display: block; @@ -232,7 +239,9 @@ export const MediaWrapper = ({ <> {children} {/* This overlay is styled when the CardLink is hovered */} - {(mediaType === 'picture' || mediaType === 'cinemagraph') && ( + {(mediaType === 'picture' || + mediaType === 'cinemagraph' || + isLoopAndInLoopClickTestVariant == true) && (
{ // The link is only applied directly to the headline if it is a sublink const isSublink = !!linkTo; @@ -247,6 +258,9 @@ export const CardHeadline = ({ isSublink ? 'card-sublink-headline' : 'card-headline' }`} css={[ + allowHeadlineToBreakWords + ? allowWordBreakStyles + : undefined, isSublink ? css` ${textSans14} diff --git a/dotcom-rendering/src/components/Carousel.island.tsx b/dotcom-rendering/src/components/Carousel.island.tsx index 74f1e8e4696..9efa8606b65 100644 --- a/dotcom-rendering/src/components/Carousel.island.tsx +++ b/dotcom-rendering/src/components/Carousel.island.tsx @@ -534,6 +534,7 @@ const CarouselCard = ({ showTopBarMobile={!isOnwardContent} aspectRatio="5:4" contentSpacing="small" + allowHeadlineToBreakWords={true} /> ); diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx index bd95b65c057..3e161de401d 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.stories.tsx @@ -1,4 +1,13 @@ import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import type { ComponentProps } from 'react'; +import { expect, waitFor, within } from 'storybook/test'; +import type { FECricketMatchHeader } from '../../frontend/feCricketMatchHeader'; +import type { + FECricketInnings, + FECricketMatch, +} from '../../frontend/feCricketMatchPage'; +import type { ArticleFormat } from '../../lib/articleFormat'; +import type { ArticleDeprecated } from '../../types/article'; import { CricketMatchHeader } from './CricketMatchHeader'; const meta = { @@ -9,258 +18,401 @@ export default meta; type Story = StoryObj; -export const Fixture = { - args: { - edition: 'UK', - match: { - kind: 'Fixture', - series: 'Ashes 2025–2026', - competition: 'Second Test Match', - venue: 'Brisbane Cricket Ground', - matchDate: new Date('2026-01-26'), - homeTeam: { - paID: 'f7f611a1-e667-2aa2-c3e0-6dbc6981cfa4', - name: 'Australia', - }, - awayTeam: { - paID: 'a359844f-fc07-9cfa-d4cc-9a9ac0d5d075', - name: 'England', - }, - innings: [], +const defaultFallOfWickets = [ + { + order: 1, + name: 'Emilio Gay', + runs: 16, + }, + { + order: 2, + name: 'Ben Duckett', + runs: 31, + }, + { + order: 3, + name: 'Jacob Bethell', + runs: 33, + }, + { + order: 4, + name: 'Joe Root', + runs: 34, + }, + { + order: 5, + name: 'Jamie Smith', + runs: 55, + }, + { + order: 6, + name: 'Ben Stokes', + runs: 94, + }, + { + order: 7, + name: 'Gus Atkinson', + runs: 108, + }, + { + order: 8, + name: 'Harry Brook', + runs: 113, + }, + { + order: 9, + name: 'Ollie Robinson', + runs: 118, + }, + { + order: 10, + name: 'Shoaib Bashir', + runs: 140, + }, +]; + +const defaultInnings = { + order: 1, + description: 'Australia 169/10 (20.0 overs)', + battingTeam: 'Australia', + bowlers: [], + batters: [], + byes: 0, + legByes: 0, + noBalls: 0, + penalties: 0, + wides: 0, + declared: false, + forfeited: false, + runsScored: 169, + overs: '20.0', + wickets: 0, + extras: 0, + fallOfWicket: defaultFallOfWickets, +} satisfies FECricketInnings; + +const baseMatch = { + teams: [ + { + name: 'Australia', + id: 'f7f611a1-e667-2aa2-c3e0-6dbc6981cfa4', + home: true, + lineup: ['David Warner', 'Travis Head'], }, - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Fixture', + { + name: 'England', + id: 'a359844f-fc07-9cfa-d4cc-9a9ac0d5d075', + home: false, + lineup: ['Zak Crawley', 'Alex Lees'], }, + ], + innings: [], + stage: 'Ashes 2025–2026', + competitionName: 'Second Test Match', + venueName: 'Brisbane Cricket Ground', + result: 'pre-match', + currentDay: 1, + totalDays: 5, + gameDate: '2026-01-26T00:00:00', + officials: [], + matchId: 'australia-v-england-second-test', +} satisfies FECricketMatch; + +const headerData = (cricketMatch: FECricketMatch): FECricketMatchHeader => ({ + cricketMatch, + liveURL: + 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', +}); + +const baseArgs = { + edition: 'UK', + selectedTab: 'info', + matchHeaderURL: + 'https://api.nextgen.guardianapps.co.uk/sport/cricket/match-header/2026-06-13/australia-women-s-cricket-team.json', + refreshInterval: 3_000, + tabContentId: 'cricket-tab-content', + getHeaderData: () => getMockData(headerData(baseMatch)), + article: {} as ArticleDeprecated, + format: {} as ArticleFormat, +} satisfies ComponentProps; + +const getMockData = (data: FECricketMatchHeader) => + new Promise((resolve) => { + setTimeout(() => resolve(data), 100); + }); + +export const Fixture = { + args: baseArgs, + play: async ({ canvas, step }) => { + await step('Fetch match header data and render UI', async () => { + await waitFor(() => + expect(canvas.getByText('Ashes 2025–2026')).toBeInTheDocument(), + ); + + await expect(canvas.getByText('England')).toBeInTheDocument(); + await expect(canvas.getByText('Australia')).toBeInTheDocument(); + + await expect(canvas.getByRole('navigation')).toBeInTheDocument(); + }); }, } satisfies Story; export const Live = { args: { - edition: 'UK', - match: { - ...Fixture.args.match, - kind: 'Live', - day: 2, - innings: [ - { - declared: false, - forfeited: false, - battingTeam: 'Australia', - runs: 169, - overs: '20.0', - fallOfWickets: 0, - }, - { - declared: false, - forfeited: false, - battingTeam: 'England', - runs: 173, - overs: '19.3', - fallOfWickets: 3, - }, - ], - }, - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Live', - liveURL: new URL( - 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + ...baseArgs, + getHeaderData: () => + Promise.resolve( + headerData({ + ...baseMatch, + result: 'in-play', + currentDay: 2, + innings: [ + { + ...defaultInnings, + description: 'Australia 169/10 (20.0 overs)', + battingTeam: 'Australia', + runsScored: 169, + overs: '20.0', + wickets: 0, + extras: 0, + fallOfWicket: [], + }, + { + ...defaultInnings, + description: 'England 173/3 (19.3 overs)', + battingTeam: 'England', + runsScored: 173, + overs: '19.3', + wickets: 3, + extras: 0, + fallOfWicket: defaultFallOfWickets.slice(0, 3), + }, + ], + }), ), - }, + selectedTab: 'info', + }, + play: async ({ canvas, step }) => { + await step('Fetch match header data and render UI', async () => { + // Wait for 'Ashes 2025–2026' to appear which signals match header + // data has been fetched and the UI rendered on the client + await canvas.findByText('Ashes 2025–2026'); + void canvas.findByText('England'); + void canvas.findByText('Australia'); + + void expect( + canvas.getByLabelText('169 runs, 0 wickets fallen'), + ).toBeInTheDocument(); + void expect( + canvas.getByLabelText('173 runs, 3 wickets fallen'), + ).toBeInTheDocument(); + + const nav = canvas.getByRole('navigation'); + const tabs = within(nav).getAllByRole('listitem'); + + void expect(tabs.length).toBe(2); + void expect(tabs[0]).toHaveTextContent('Live feed'); + void expect(tabs[1]).toHaveTextContent('Scorecard'); + }); }, } satisfies Story; export const LiveYetToBat = { name: 'Live (Team yet to bat)', args: { - edition: 'UK', - match: { - ...Fixture.args.match, - kind: 'Live', - day: 2, - innings: [ - { - declared: false, - forfeited: false, - battingTeam: 'Australia', - runs: 169, - overs: '20.0', - fallOfWickets: 10, - }, - ], - }, - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Live', - liveURL: new URL( - 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + ...baseArgs, + getHeaderData: () => + Promise.resolve( + headerData({ + ...baseMatch, + result: 'in-play', + currentDay: 2, + innings: [ + { + ...defaultInnings, + description: 'Australia 169 all out (20.0 overs)', + battingTeam: 'Australia', + runsScored: 169, + overs: '20.0', + wickets: 10, + extras: 0, + fallOfWicket: defaultFallOfWickets, + }, + ], + }), ), - }, + selectedTab: 'info', }, } satisfies Story; export const Result = { args: { - edition: 'UK', - match: { - ...Fixture.args.match, - kind: 'Result', - day: 4, - innings: [ - { - declared: false, - forfeited: false, - battingTeam: 'Australia', - runs: 245, - overs: '65.0', - fallOfWickets: 10, - }, - { - declared: false, - forfeited: false, - battingTeam: 'England', - runs: 361, - overs: '89.5', - fallOfWickets: 10, - }, - { - declared: false, - forfeited: false, - battingTeam: 'Australia', - runs: 258, - overs: '78.1', - fallOfWickets: 10, - }, - { - declared: false, - forfeited: false, - battingTeam: 'England', - runs: 364, - overs: '92.5', - fallOfWickets: 10, - }, - ], - result: { - type: 'home-win', - description: 'Australia win by an innings and 47 runs', - winner: { - type: 'runs', - team: 'Australia', - margin: 47, - }, - }, - }, - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Result', - liveURL: new URL( - 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + ...baseArgs, + getHeaderData: () => + Promise.resolve( + headerData({ + ...baseMatch, + result: 'result', + currentDay: 4, + innings: [ + { + ...defaultInnings, + battingTeam: 'Australia', + runsScored: 245, + overs: '65.0', + wickets: 10, + extras: 0, + }, + { + ...defaultInnings, + battingTeam: 'England', + runsScored: 361, + overs: '89.5', + wickets: 10, + extras: 0, + }, + { + ...defaultInnings, + battingTeam: 'Australia', + runsScored: 258, + overs: '78.1', + wickets: 10, + extras: 0, + }, + { + ...defaultInnings, + battingTeam: 'England', + runsScored: 364, + overs: '92.5', + wickets: 10, + extras: 0, + }, + ], + fullResult: { + resultType: 'home-win', + description: 'Australia win by an innings and 47 runs', + winner: { + winType: 'runs', + team: 'Australia', + margin: '47', + }, + }, + }), ), - }, + selectedTab: 'info', + }, + play: async ({ canvas, step }) => { + await step('Fetch match header data and render UI', async () => { + // Wait for 'Ashes 2025–2026' to appear which signals match header + // data has been fetched and the UI rendered on the client + await canvas.findByText('Ashes 2025–2026'); + void canvas.findByText('England'); + void canvas.findByText('Australia'); + + const nav = canvas.getByRole('navigation'); + const tabs = within(nav).getAllByRole('listitem'); + + void expect(tabs.length).toBe(2); + void expect(tabs[0]).toHaveTextContent('Live feed'); + void expect(tabs[1]).toHaveTextContent('Scorecard'); + }); }, } satisfies Story; export const ResultWinByWickets = { args: { - edition: 'UK', - match: { - ...Fixture.args.match, - kind: 'Result', - innings: [ - { - declared: false, - forfeited: false, - battingTeam: 'Australia', - runs: 186, - overs: '16.2', - fallOfWickets: 3, - }, - { - declared: false, - forfeited: false, - battingTeam: 'England', - runs: 182, - overs: '20.0', - fallOfWickets: 8, - }, - ], - result: { - type: 'home-win', - winner: { - type: 'wickets', - team: 'Australia', - margin: 2, - }, - }, - }, - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Result', - liveURL: new URL( - 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + ...baseArgs, + getHeaderData: () => + Promise.resolve( + headerData({ + ...baseMatch, + result: 'result', + innings: [ + { + ...defaultInnings, + battingTeam: 'Australia', + runsScored: 186, + overs: '16.2', + wickets: 3, + extras: 0, + fallOfWicket: defaultFallOfWickets.slice(0, 3), + }, + { + ...defaultInnings, + battingTeam: 'England', + runsScored: 182, + overs: '20.0', + wickets: 8, + extras: 0, + fallOfWicket: defaultFallOfWickets.slice(0, 8), + }, + ], + fullResult: { + resultType: 'home-win', + winner: { + winType: 'wickets', + team: 'Australia', + margin: '2', + }, + }, + }), ), - }, + selectedTab: 'info', }, } satisfies Story; export const ResultDrawn = { args: { - edition: 'UK', - match: { - ...Fixture.args.match, - kind: 'Result', - day: 4, - innings: [ - { - declared: false, - forfeited: false, - battingTeam: 'Australia', - runs: 302, - overs: '120.3', - fallOfWickets: 10, - }, - { - declared: false, - forfeited: false, - battingTeam: 'England', - runs: 226, - overs: '60.2', - fallOfWickets: 10, - }, - { - declared: true, - forfeited: false, - battingTeam: 'Australia', - runs: 218, - overs: '72.5', - fallOfWickets: 5, - }, - { - declared: false, - forfeited: false, - battingTeam: 'England', - runs: 239, - overs: '67.4', - fallOfWickets: 7, - }, - ], - result: { - type: 'draw', - }, - }, - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Result', - liveURL: new URL( - 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + ...baseArgs, + getHeaderData: () => + Promise.resolve( + headerData({ + ...baseMatch, + result: 'result', + currentDay: 4, + innings: [ + { + ...defaultInnings, + battingTeam: 'Australia', + runsScored: 302, + overs: '120.3', + wickets: 10, + extras: 0, + }, + { + ...defaultInnings, + battingTeam: 'England', + runsScored: 226, + overs: '60.2', + wickets: 10, + extras: 0, + }, + { + ...defaultInnings, + declared: true, + battingTeam: 'Australia', + runsScored: 218, + overs: '72.5', + wickets: 5, + extras: 0, + fallOfWicket: defaultFallOfWickets.slice(0, 5), + }, + { + ...defaultInnings, + forfeited: false, + battingTeam: 'England', + runsScored: 239, + overs: '67.4', + wickets: 7, + extras: 0, + fallOfWicket: defaultFallOfWickets.slice(0, 7), + }, + ], + fullResult: { + resultType: 'draw', + }, + }), ), - }, + selectedTab: 'info', }, } satisfies Story; diff --git a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx index 0c00cc57c28..a86e40a7b11 100644 --- a/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx +++ b/dotcom-rendering/src/components/CricketMatchHeader/CricketMatchHeader.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/react'; +import { log } from '@guardian/libs'; import { from, headlineBold20Object, @@ -13,24 +14,28 @@ import { textSansItalic15Object, until, } from '@guardian/source/foundations'; -import type { ComponentProps } from 'react'; -import { Fragment, type ReactNode, useMemo } from 'react'; +import { Fragment, type ReactNode, useEffect, useMemo, useState } from 'react'; +import type { SWRConfiguration } from 'swr'; +import useSWR from 'swr'; import type { CricketMatch, + CricketResult, CricketTeam, - InningsOverview, - Result, } from '../../cricketMatchV2'; import { grid } from '../../grid'; +import { ArticleDesign, type ArticleFormat } from '../../lib/articleFormat'; import { type EditionId, getLocaleFromEdition, getTimeZoneFromEdition, } from '../../lib/edition'; import { generateImageURL } from '../../lib/image'; +import { useLocationHash } from '../../lib/useLocationHash'; import { palette } from '../../palette'; import type { ColourName } from '../../paletteDeclarations'; +import type { ArticleDeprecated } from '../../types/article'; import { BigNumber } from '../BigNumber'; +import { CricketScorecardTabRemoteRender } from '../CricketScorecardTabRemoteRender'; import { background, border, @@ -38,15 +43,90 @@ import { secondaryText, } from '../FootballMatchHeader/colours'; import { Tabs } from '../FootballMatchHeader/Tabs'; +import { MatchHeaderFallback } from '../MatchHeaderFallback'; +import { Placeholder } from '../Placeholder'; +import type { CricketHeaderData } from './headerData'; +import { parse as parseHeaderData } from './headerData'; -type Props = { +export type CricketMatchHeaderProps = { + matchHeaderURL: string; edition: EditionId; - match: CricketMatch; - tabs: ComponentProps; + selectedTab: 'info' | 'live' | 'report'; + tabContentId: string; + format: ArticleFormat; + article: ArticleDeprecated; +}; + +type Props = CricketMatchHeaderProps & { + getHeaderData: (url: string) => Promise; + refreshInterval: number; }; export const CricketMatchHeader = (props: Props) => { - const match = props.match; + const scorecardHashbang = '#scorecard'; + const locationHash = useLocationHash(); + + const { data, error } = useSWR( + props.matchHeaderURL, + fetcher(props.getHeaderData), + swrOptions(props.refreshInterval), + ); + + const [selectedTab, setSelectedTab] = useState<'info' | 'live' | 'report'>( + props.selectedTab, + ); + + const [tabContentElement, setTabContentElement] = + useState(null); + + useEffect(() => { + if (locationHash === scorecardHashbang) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- we want to set the selected tab based on the hashbang in the URL + setSelectedTab('info'); + } + }, [locationHash]); + + useEffect(() => { + const el = document.getElementById(props.tabContentId); + if (el) { + // eslint-disable-next-line react-hooks/set-state-in-effect -- We need to capture the element client side + setTabContentElement(el); + } + }, [props.tabContentId]); + + if (error) { + if ( + props.format.design === ArticleDesign.LiveBlog || + props.format.design === ArticleDesign.DeadBlog + ) { + return ( + + ); + } + } + + if (data === undefined) { + return ( + + ); + } + + const { match, tabs } = data; + + const onInfoTabClick = () => { + setSelectedTab('info'); + window.location.hash = scorecardHashbang; + }; return (
{ >
@@ -73,12 +157,59 @@ export const CricketMatchHeader = (props: Props) => { {match.result && }
- +
+ {selectedTab === 'info' && ( + + )}
); }; +const swrOptions = ( + refreshInterval: number, +): SWRConfiguration => ({ + errorRetryCount: 1, + refreshInterval: (latestData: CricketHeaderData | undefined) => { + return latestData?.match.kind === 'Live' || + latestData?.match.kind === 'Fixture' + ? refreshInterval + : 0; + }, +}); + +const fetcher = + (getHeaderData: Props['getHeaderData']) => + (url: string): Promise => + getHeaderData(url) + .then(parseHeaderData) + .then((result) => { + if (!result.ok) { + log('dotcom', result.error); + throw new Error(); + } else { + return result.value; + } + }) + .catch(() => { + log('dotcom', 'Failed to fetch match header json'); + throw new Error(); + }); + const StatusLine = (props: { match: CricketMatch; edition: EditionId }) => (

{ flex: '1 1 50%', wordBreak: 'break-word', borderLeftStyle: 'solid', + borderLeftColor: 'var(--border-left-colour)', '&:last-of-type': { paddingLeft: space[2], borderLeftWidth: 1, }, [from.leftCol]: { - paddingLeft: space[2], - borderLeftWidth: 1, paddingTop: space[3], + position: 'relative', + '&:first-of-type::before': { + content: '""', + top: 0, + left: -10, + width: 1, + backgroundColor: 'var(--border-left-colour)', + position: 'absolute', + height: '100%', + }, }, }} style={{ - borderLeftColor: palette(border(props.match.kind)), + '--border-left-colour': palette(border(props.match.kind)), }} > @@ -271,13 +411,20 @@ const Team = (props: { team: CricketTeam; match: CricketMatch }) => { innings.map((inning, index) => ( - {!!inning.overs && ( + {!!inning.inningsTotals.overs && ( <> - + { ), }} > - {inning.overs} overs + {inning.inningsTotals.overs} overs )} @@ -313,13 +460,19 @@ const Team = (props: { team: CricketTeam; match: CricketMatch }) => { ); }; -const EndOfInningReason = (props: { inning: InningsOverview }) => { +const EndOfInningReason = (props: { + inning: { + wickets: number; + declared: boolean; + forfeited: boolean; + }; +}) => { const styles = { ...textSans14Object, marginRight: space[1], }; - if (props.inning.fallOfWickets === 10) { + if (props.inning.wickets === 10) { return All out; } if (props.inning.declared) { @@ -510,7 +663,7 @@ const crestUrl = (teamId: string): URL | undefined => { * But in some cases, we just get the data about the result and need to generate * a description from that. */ -const resultDescription = (result: Result): string => { +const resultDescription = (result: CricketResult): string => { if (result.description) { return result.description; } @@ -546,7 +699,7 @@ const resultDescription = (result: Result): string => { } }; -const ResultLine = (props: { result: Result }) => { +const ResultLine = (props: { result: CricketResult }) => { const description = resultDescription(props.result); return (

=> { + const feData = fromValibot(safeParse(feCricketMatchHeaderSchema, json)); + + if (!feData.ok) { + return error('Failed to validate match header json'); + } + + const parsedMatch = parseCricketMatchV2(feData.value.cricketMatch); + + if (!parsedMatch.ok) { + return error('Failed to parse the match from the header json'); + } + + const maybeTabs = createTabs(feData.value, parsedMatch.value.kind); + + if (!maybeTabs.ok) { + return error( + `The match header data contained an invalid ${maybeTabs.error.kind} URL`, + ); + } + + return ok({ + match: parsedMatch.value, + tabs: maybeTabs.value, + }); +}; + +type MatchURLError = { + kind: 'live' | 'report'; +}; + +const createTabs = ( + feData: FECricketMatchHeader, + matchKind: CricketMatch['kind'], +): Result => { + const reportURL = + feData.reportURL !== undefined + ? safeParseURL(feData.reportURL) + : undefined; + const liveURL = + feData.liveURL !== undefined ? safeParseURL(feData.liveURL) : undefined; + if (reportURL !== undefined && !reportURL.ok) { + return error({ kind: 'report' }); + } + + if (liveURL !== undefined && !liveURL.ok) { + return error({ kind: 'live' }); + } + + return ok({ + matchKind, + sportKind: 'cricket', + reportURL: reportURL?.value, + liveURL: liveURL?.value, + }); +}; diff --git a/dotcom-rendering/src/components/CricketMatchHeaderTabsDemo.stories.tsx b/dotcom-rendering/src/components/CricketMatchHeaderTabsDemo.stories.tsx new file mode 100644 index 00000000000..2a894066a7e --- /dev/null +++ b/dotcom-rendering/src/components/CricketMatchHeaderTabsDemo.stories.tsx @@ -0,0 +1,156 @@ +import type { ComponentProps } from 'react'; +import { expect, userEvent, waitFor } from 'storybook/test'; +import { SWRConfig } from 'swr'; +import { allModes } from '../../.storybook/modes'; +import preview from '../../.storybook/preview'; +import { liveMatch, resultMatch } from '../../fixtures/manual/cricketMatch'; +import type { FECricketMatchHeader } from '../frontend/feCricketMatchHeader'; +import type { ArticleFormat } from '../lib/articleFormat'; +import type { ArticleDeprecated } from '../types/article'; +import { CricketMatchHeader } from './CricketMatchHeader/CricketMatchHeader'; + +const meta = preview.meta({ + component: CricketMatchHeader, + title: 'Components/CricketMatchHeaderTabsDemo', + parameters: { + chromatic: { + modes: { + 'light leftCol': allModes['light leftCol'], + }, + }, + }, + decorators: [ + (Story) => ( + new Map(), + refreshInterval: 100, + dedupingInterval: 100, + }} + > +
+ +
Initial Tab content
+
+
+ ), + ], +}); + +const baseArgs = { + matchHeaderURL: + 'https://api.nextgen.guardianapps.co.uk/football/api/match-header/2026/02/08/26247/48490.json', + selectedTab: 'live' as 'info' | 'live' | 'report', + edition: 'UK', + tabContentId: 'cricket-tab-content', + refreshInterval: 60_000, + getHeaderData: () => + getMockData({ + cricketMatch: liveMatch, + liveURL: + 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + }), + article: {} as ArticleDeprecated, + format: {} as ArticleFormat, +} satisfies ComponentProps; + +const liveArgs = { + ...baseArgs, + selectedTab: 'info', +} satisfies ComponentProps; + +export const CricketScorecardPageNewFixture = meta.story({ + name: 'Live', + args: baseArgs, +}); + +export const CricketScorecardPageNewLive = meta.story({ + name: 'Scorecard', + args: liveArgs, +}); + +export const ClickScorecardTab = meta.story({ + name: 'Live -> Scorecard', + args: { ...liveArgs, selectedTab: 'live' }, + play: async ({ canvas }) => { + await canvas.findByText('Second Test Match'); + + // Click the Scorecard tab button + const scorecardTab = canvas.getByRole('button', { name: 'Scorecard' }); + await userEvent.click(scorecardTab); + + // Check that the scorecard renders in the main content element + await expect( + await canvas.findByRole('heading', { + name: 'Lineups', + }), + ).toBeInTheDocument(); + }, +}); + +let matchType = 'live' as 'live' | 'result'; + +export const UpdateScorecardTab = meta.story({ + name: 'Scorecard Update', + beforeEach: () => { + matchType = 'live'; + }, + args: { + ...liveArgs, + getHeaderData: async () => { + const value = (await getChangingHeaderData.next()).value; + return value; + }, + refreshInterval: 100, + }, + play: async ({ canvas }) => { + await canvas.findByRole('heading', { + name: 'New Zealand first innings', + }); + + await expect( + canvas.queryByText('England win by 115 runs'), + ).not.toBeInTheDocument(); + + await expect( + canvas.queryByRole('rowheader', { name: /Harry Brook/ }) + ?.nextSibling!.textContent, + ).toBe('Yet to Bat'); + + matchType = 'result'; + + await waitFor(() => { + void expect( + canvas.getByText('England win by 115 runs'), + ).toBeInTheDocument(); + }); + + await waitFor(() => { + void expect( + canvas.queryByRole('rowheader', { name: /Harry Brook/ }) + ?.nextSibling!.textContent, + ).toBe('lbw b Henry'); + }); + }, +}); + +const getMockData = (data: FECricketMatchHeader) => Promise.resolve(data); + +const getChangingHeaderData = (async function* () { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- This is a test generator function + while (true) { + if (matchType === 'live') { + yield getMockData({ + cricketMatch: liveMatch, + liveURL: + 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + }); + } else { + yield getMockData({ + cricketMatch: resultMatch, + liveURL: + 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', + }); + } + } +})(); diff --git a/dotcom-rendering/src/components/CricketMatchHeaderWrapper.island.tsx b/dotcom-rendering/src/components/CricketMatchHeaderWrapper.island.tsx new file mode 100644 index 00000000000..e27a988198c --- /dev/null +++ b/dotcom-rendering/src/components/CricketMatchHeaderWrapper.island.tsx @@ -0,0 +1,15 @@ +import type { CricketMatchHeaderProps } from './CricketMatchHeader/CricketMatchHeader'; +import { CricketMatchHeader } from './CricketMatchHeader/CricketMatchHeader'; + +export const CricketMatchHeaderWrapper = (props: CricketMatchHeaderProps) => { + return ( + + ); +}; + +const getHeaderData = (url: string): Promise => + fetch(url).then((res) => res.json()); diff --git a/dotcom-rendering/src/components/CricketMatchStat.tsx b/dotcom-rendering/src/components/CricketMatchStat.tsx new file mode 100644 index 00000000000..84e2927e3be --- /dev/null +++ b/dotcom-rendering/src/components/CricketMatchStat.tsx @@ -0,0 +1,147 @@ +import { css } from '@emotion/react'; +import { + from, + space, + textSans14, + textSans15, + textSansBold14, + textSansBold15, + visuallyHidden, +} from '@guardian/source/foundations'; +import type { Batter } from '../cricketMatchV2'; +import { palette } from '../palette'; + +const containerCss = css` + position: relative; + padding: 5px 10px 10px; + color: ${palette('--football-match-stat-text')}; + border: 1px solid ${palette('--football-match-stat-border')}; + border-radius: 6px; +`; + +const desktopPaddingCss = css` + ${from.desktop} { + padding-bottom: 14px; + } +`; + +const visuallyHiddenStyles = css` + ${visuallyHidden} +`; + +const responsiveTextSans = css` + ${textSans14} + ${from.desktop} { + ${textSans15} + } +`; + +const responsiveTextSansBold = css` + ${textSansBold14} + ${from.desktop} { + ${textSansBold15} + } +`; + +const tableStyles = css` + width: 100%; + border-collapse: collapse; + ${responsiveTextSans} +`; + +const cellBaseStyles = css` + padding: ${space[2]}px ${space[3]}px ${space[1]}px 0; + text-align: left; + vertical-align: middle; +`; + +const tableHeadCellStyles = css` + ${cellBaseStyles} + ${responsiveTextSansBold} + color: ${palette('--football-match-stat-text')}; +`; + +const tableCellStyles = css` + ${cellBaseStyles} + ${responsiveTextSans} +`; + +const tableRowHeaderStyles = css` + ${cellBaseStyles} + display: flex; + align-items: center; + ${responsiveTextSans} +`; + +const batterNameTextStyles = css` + display: flex; + flex-direction: column; +`; + +const tableRowStyles = css` + border-top: 1px solid ${palette('--football-match-stat-border')}; +`; + +const numericCellStyles = css` + white-space: nowrap; + text-align: left; +`; + +const howOutStyles = css` + color: ${palette('--football-match-info-team-number')}; +`; + +export const CricketMatchStatNotOutBatters = ({ + notOutBatters, +}: { + notOutBatters: Batter[]; +}) => { + const currentBatters = notOutBatters.filter( + (batter) => batter.onStrike || batter.nonStrike, + ); + return ( +
+ Current Batters + + + + + + + + + + {currentBatters.map((batter) => { + return ( + + + + + + ); + })} + +
Batter + Runs + + Balls +
+ + {batter.onStrike + ? '(on strike)' + : '(at crease)'} + +
+ {batter.name} +
+ {batter.howOut} +
+
+
+ {batter.runs} + + {batter.ballsFaced} +
+
+ ); +}; diff --git a/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx b/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx new file mode 100644 index 00000000000..3b56f24846b --- /dev/null +++ b/dotcom-rendering/src/components/CricketMiniMatchStats.stories.tsx @@ -0,0 +1,100 @@ +import { css } from '@emotion/react'; +import { breakpoints, from } from '@guardian/source/foundations'; +import preview from '../../.storybook/preview'; +import type { FECricketMatchStatsSummary } from '../frontend/feCricketMatchPage'; +import { palette } from '../palette'; +import { CricketMiniMatchStats as CricketMiniMatchStatsComponent } from './CricketMiniMatchStats'; + +const gridCss = css` + background-color: ${palette('--football-live-blog-background')}; + /** + * Extremely simplified live blog grid layout as we're only interested in + * the 240px wide left column added at the desktop breakpoint. + * dotcom-rendering/src/layouts/LiveLayout.tsx + */ + ${from.desktop} { + display: grid; + grid-column-gap: 20px; + grid-template-columns: 240px 1fr; + } +`; + +const meta = preview.meta({ + title: 'Components/Cricket Mini Match Stats', + component: CricketMiniMatchStatsComponent, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + chromatic: { + viewports: [ + breakpoints.mobileMedium, + breakpoints.tablet, + breakpoints.wide, + ], + }, + }, +}); + +const feMatchStatsSummaryData: FECricketMatchStatsSummary = { + status: 'abandoned', + currentBattingTeam: 'England', + notOutBatters: [ + { + name: 'Tom Latham', + order: 1, + ballsFaced: 214, + runs: 151, + fours: 15, + sixes: 0, + out: false, + howOut: 'not out', + onStrike: false, + nonStrike: true, + }, + { + name: 'Devon Conway', + order: 2, + ballsFaced: 224, + runs: 157, + fours: 22, + sixes: 3, + out: false, + howOut: 'not out', + onStrike: true, + nonStrike: false, + }, + { + name: 'Devon Conway', + order: 2, + ballsFaced: 224, + runs: 157, + fours: 22, + sixes: 3, + out: false, + howOut: 'not out', + onStrike: false, + nonStrike: false, + }, + ], +}; + +const getMockData = (data: FECricketMatchStatsSummary) => + new Promise((resolve) => { + setTimeout(() => { + resolve(data); + }, 1000); + }); + +export const CricketMiniMatchStats = meta.story({ + args: { + matchStatsUrl: + 'https://api.nextgen.guardianapps.co.uk/sport/cricket/match-stats/2026-06-25/england-cricket-team.json', + getMatchStatsData: () => getMockData(feMatchStatsSummaryData), + refreshInterval: 16_000, + }, +}); diff --git a/dotcom-rendering/src/components/CricketMiniMatchStats.tsx b/dotcom-rendering/src/components/CricketMiniMatchStats.tsx new file mode 100644 index 00000000000..fe70ee8438d --- /dev/null +++ b/dotcom-rendering/src/components/CricketMiniMatchStats.tsx @@ -0,0 +1,153 @@ +import { css } from '@emotion/react'; +import { log } from '@guardian/libs'; +import { from } from '@guardian/source/foundations'; +import { + LinkButton, + SvgArrowRightStraight, +} from '@guardian/source/react-components'; +import type { SWRConfiguration } from 'swr'; +import useSWR from 'swr'; +import { safeParse } from 'valibot'; +import type { CricketMatchStatsSummary } from '../cricketMatchV2'; +import { parseMatchStatsSummary } from '../cricketMatchV2'; +import { feCricketMatchStatsSummarySchema } from '../frontend/feCricketMatchPage'; +import type { Result } from '../lib/result'; +import { error, fromValibot, ok } from '../lib/result'; +import { palette } from '../palette'; +import { CricketMatchStatNotOutBatters } from './CricketMatchStat'; +import { Placeholder } from './Placeholder'; + +const containerCss = css` + isolation: isolate; + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + background-color: ${palette('--football-live-blog-background')}; + ${from.mobileLandscape} { + padding: 20px; + } + ${from.desktop} { + padding-top: 24px; + padding-right: 0; + } +`; + +const buttonTextCss = css` + ${from.desktop} { + display: none; + } +`; + +const buttonTextShortCss = css` + display: none; + ${from.desktop} { + display: inline; + } +`; + +type Props = { + matchStatsUrl: string; + getMatchStatsData: (url: string) => Promise; + refreshInterval: number; +}; + +export const CricketMiniMatchStats = (props: Props) => { + const { data, error: swrError } = useSWR( + props.matchStatsUrl, + fetcher(props.getMatchStatsData), + swrOptions(props.refreshInterval), + ); + + if (swrError) { + return null; + } + + if (data === undefined) { + return ( + + ); + } + + if (data.notOutBatters == null) { + return null; + } + + return ( +
+ + + } + iconSide="right" + theme={{ + backgroundPrimary: palette( + '--football-match-stat-button-background', + ), + backgroundPrimaryHover: palette( + '--football-match-stat-button-background-hover', + ), + }} + > + View full scorecard + View full scorecard + +
+ ); +}; + +const isMatchOver = (matchStatus: string | undefined) => + matchStatus === 'result' || matchStatus === 'abandoned'; + +const swrOptions = ( + refreshInterval: number, +): SWRConfiguration => ({ + errorRetryCount: 1, + refreshInterval: (latestData: CricketMatchStatsSummary | undefined) => + isMatchOver(latestData?.status) ? 0 : refreshInterval, +}); + +const fetcher = + (getMatchStatsData: Props['getMatchStatsData']) => + (url: string): Promise => + getMatchStatsData(url) + .then(parseData) + .then((result) => { + if (!result.ok) { + log('dotcom', result.error); + throw new Error(); + } else { + return result.value; + } + }) + .catch(() => { + log('dotcom', 'Failed to fetch math stats summary data'); + throw new Error(); + }); + +const parseData = (json: unknown): Result => { + const feData = fromValibot( + safeParse(feCricketMatchStatsSummarySchema, json), + ); + + if (!feData.ok) { + return error('Failed to validate match stats summary data'); + } + + const parsedMatchStats = parseMatchStatsSummary(feData.value); + + if (!parsedMatchStats.ok) { + return error('Failed to parse match stats summary'); + } + + return ok(parsedMatchStats.value); +}; diff --git a/dotcom-rendering/src/components/CricketMiniMatchStatsWrapper.island.tsx b/dotcom-rendering/src/components/CricketMiniMatchStatsWrapper.island.tsx new file mode 100644 index 00000000000..691ca75f4b4 --- /dev/null +++ b/dotcom-rendering/src/components/CricketMiniMatchStatsWrapper.island.tsx @@ -0,0 +1,17 @@ +import { CricketMiniMatchStats } from './CricketMiniMatchStats'; + +export const CricketMiniMatchStatsWrapper = ({ + matchStatsUrl, +}: { + matchStatsUrl: string; +}) => ( + +); + +const getMatchStatsData = (url: string): Promise => { + return fetch(url).then((res) => res.json()); +}; diff --git a/dotcom-rendering/src/components/CricketScoreboard.test.tsx b/dotcom-rendering/src/components/CricketScoreboard.test.tsx index e0c13eaf1ef..8f01d3fbf28 100644 --- a/dotcom-rendering/src/components/CricketScoreboard.test.tsx +++ b/dotcom-rendering/src/components/CricketScoreboard.test.tsx @@ -14,7 +14,8 @@ const defaultInnings: CricketInnings = { const defaultMatch: CricketMatch = { matchId: 'test', - competitionName: 'test match', + competitionName: 'test match series', + stage: 'test match', venueName: 'venue', gameDate: '2021-06-30T14:00:00', teams: [ diff --git a/dotcom-rendering/src/components/CricketScoreboard.tsx b/dotcom-rendering/src/components/CricketScoreboard.tsx index 63dad8975a6..50dc9255ca7 100644 --- a/dotcom-rendering/src/components/CricketScoreboard.tsx +++ b/dotcom-rendering/src/components/CricketScoreboard.tsx @@ -144,7 +144,7 @@ export const CricketScoreboard = ({ scorecardUrl, match }: Props) => { showDate={true} showTime={false} /> - {match.competitionName}, {match.venueName} + {match.stage}, {match.venueName} @@ -174,7 +174,7 @@ export const CricketScoreboard = ({ scorecardUrl, match }: Props) => { diff --git a/dotcom-rendering/src/components/CricketScorecardNew.stories.tsx b/dotcom-rendering/src/components/CricketScorecardNew.stories.tsx index 187ecf6ff98..db4c84dec67 100644 --- a/dotcom-rendering/src/components/CricketScorecardNew.stories.tsx +++ b/dotcom-rendering/src/components/CricketScorecardNew.stories.tsx @@ -23,6 +23,8 @@ export const CricketScorecardNew = meta.story({ { description: 'India first innings', battingTeam: 'India', + declared: false, + forfeited: false, inningsTotals: { runs: 254, overs: '49.0', @@ -186,6 +188,8 @@ export const CricketScorecardNew = meta.story({ { description: 'New Zealand first innings', battingTeam: 'New Zealand', + declared: false, + forfeited: false, inningsTotals: { runs: 254, overs: '49.0', @@ -336,13 +340,7 @@ export const CricketScorecardNew = meta.story({ homeTeam: { paID: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', name: 'India', - }, - awayTeam: { - paID: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', - name: 'New Zealand', - }, - lineups: { - homeTeam: [ + lineup: [ 'Rohit Sharma', 'Shubman Gill', 'Virat Kohli', @@ -355,7 +353,11 @@ export const CricketScorecardNew = meta.story({ 'Kuldeep Yadav', 'Varun Chakaravarthy', ], - awayTeam: [ + }, + awayTeam: { + paID: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + name: 'New Zealand', + lineup: [ 'Rachin Ravindra', 'Will Young', 'Kane Williamson', diff --git a/dotcom-rendering/src/components/CricketScorecardNew.tsx b/dotcom-rendering/src/components/CricketScorecardNew.tsx index 0e34a9c9d21..18d66d3aa08 100644 --- a/dotcom-rendering/src/components/CricketScorecardNew.tsx +++ b/dotcom-rendering/src/components/CricketScorecardNew.tsx @@ -12,12 +12,12 @@ import { type ReactNode } from 'react'; import type { Batter, Bowler, + CricketResult, CricketTeam, Extras, FallOfWicket, Innings, InningsTotals, - Result, } from '../cricketMatchV2'; import { palette } from '../palette'; @@ -253,11 +253,11 @@ const teamNameStyles = css` `; const homeTeamNameStyles = css` - color: ${palette('--cricket-scorecard-first-team-color')}; + color: ${palette('--cricket-scorecard-first-team-lineup-color')}; `; const awayTeamNameStyles = css` - color: ${palette('--cricket-scorecard-second-team-color')}; + color: ${palette('--cricket-scorecard-second-team-lineup-color')}; `; const playerListStyles = css` @@ -631,11 +631,9 @@ const FallOfWickets = ({ const LineupTeam = ({ team, teamType, - lineup, }: { team: CricketTeam; teamType: 'homeTeam' | 'awayTeam'; - lineup: string[]; }) => (
    - {lineup.map((player) => ( + {team.lineup.map((player) => (
  • {player}
  • @@ -668,11 +666,7 @@ type Props = { officials: string[]; homeTeam: CricketTeam; awayTeam: CricketTeam; - matchResult?: Result; - lineups: { - homeTeam: string[]; - awayTeam: string[]; - }; + matchResult?: CricketResult; }; export const CricketScorecardNew = ({ @@ -681,7 +675,6 @@ export const CricketScorecardNew = ({ homeTeam, awayTeam, matchResult, - lineups, }: Props) => (
    {allInnings.map((innings, index) => { @@ -723,16 +716,8 @@ export const CricketScorecardNew = ({

    Lineups

    - - + +
    diff --git a/dotcom-rendering/src/components/CricketScorecardPageNew.stories.tsx b/dotcom-rendering/src/components/CricketScorecardPageNew.stories.tsx deleted file mode 100644 index 5c96803064a..00000000000 --- a/dotcom-rendering/src/components/CricketScorecardPageNew.stories.tsx +++ /dev/null @@ -1,427 +0,0 @@ -import type { ComponentProps } from 'react'; -import { allModes } from '../../.storybook/modes'; -import preview from '../../.storybook/preview'; -import { CricketScorecardPageNew as CricketScorecardPageNewComponent } from './CricketScorecardPageNew'; - -const meta = preview.meta({ - component: CricketScorecardPageNewComponent, - title: 'Components/CricketScorecardPageNew', - parameters: { - chromatic: { - modes: { - 'light leftCol': allModes['light leftCol'], - }, - }, - }, -}); - -const baseArgs = { - match: { - kind: 'Fixture', - series: 'Ashes 2025–2026', - competition: 'Second Test Match', - venue: 'Bengaluru Cricket Ground', - matchDate: new Date('2026-01-26'), - homeTeam: { - paID: 'f822b9f9-9fdc-399f-54f9-e621edaf0a28', - name: 'India', - }, - awayTeam: { - paID: '110c70b5-c05f-3be7-6670-baecd50a8c6b', - name: 'New Zealand', - }, - innings: [], - }, - allInnings: [], - officials: [ - 'P R Reiffel', - 'R K Illingworth', - 'J S Wilson', - 'H D P K Dharmasena', - 'R S Madugalle', - ], - lineups: { - homeTeam: [ - 'Rohit Sharma', - 'Shubman Gill', - 'Virat Kohli', - 'Shreyas Iyer', - 'Axar Patel', - 'Lokesh Rahul', - 'Hardik Pandya', - 'Ravindra Jadeja', - 'Mohammed Shami', - 'Kuldeep Yadav', - 'Varun Chakaravarthy', - ], - awayTeam: [ - 'Rachin Ravindra', - 'Will Young', - 'Kane Williamson', - 'Daryl Mitchell', - 'Tom Latham', - 'Glenn Phillips', - 'Michael Bracewell', - 'Mitchell Santner', - 'Nathan Smith', - 'Kyle Jamieson', - "Will O'Rourke", - ], - }, - edition: 'UK', - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Fixture', - }, -} satisfies ComponentProps; - -export const CricketScorecardPageNewFixture = meta.story({ - name: 'Cricket Scorecard Page Fixture (New)', - args: baseArgs, -}); - -export const CricketScorecardPageNewLive = meta.story({ - name: 'Cricket Scorecard Page Live (New)', - args: { - ...baseArgs, - tabs: { - selected: 'info', - sportKind: 'cricket', - matchKind: 'Live', - liveURL: new URL( - 'https://www.theguardian.com/sport/live/2026/jan/27/australia-v-england-second-test-day-two-live-cricket', - ), - }, - match: { - ...baseArgs.match, - kind: 'Live', - day: 2, - innings: [ - { - declared: false, - forfeited: false, - battingTeam: 'India', - runs: 254, - overs: '49.0', - fallOfWickets: 3, - }, - { - declared: false, - forfeited: false, - battingTeam: 'New Zealand', - runs: 254, - overs: '49.0', - fallOfWickets: 2, - }, - ], - }, - allInnings: [ - { - description: 'India first innings', - battingTeam: 'India', - inningsTotals: { - runs: 254, - overs: '49.0', - extras: 8, - wickets: 7, - }, - extras: { - byes: 1, - legByes: 2, - wides: 5, - noBalls: 0, - penalties: 0, - }, - batters: [ - { - name: 'Rohit Sharma', - ballsFaced: 83, - runs: 76, - fours: 7, - sixes: 3, - howOut: 'st Latham b Ravindra', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Shubman Gill', - ballsFaced: 50, - runs: 31, - fours: 0, - sixes: 1, - howOut: 'c Phillips b Santner', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Virat Kohli', - ballsFaced: 2, - runs: 1, - fours: 0, - sixes: 0, - howOut: 'lbw b Bracewell', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Shreyas Iyer', - ballsFaced: 62, - runs: 48, - fours: 2, - sixes: 2, - howOut: 'c Ravindra b Santner', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Lokesh Rahul', - ballsFaced: 45, - runs: 39, - fours: 3, - sixes: 0, - howOut: 'run out (Williamson)', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Hardik Pandya', - ballsFaced: 18, - runs: 22, - fours: 2, - sixes: 1, - howOut: 'c Bracewell b Smith', - out: true, - nonStrike: true, - onStrike: false, - }, - { - name: 'Ravindra Jadeja', - ballsFaced: 24, - runs: 30, - fours: 1, - sixes: 0, - howOut: 'not out', - out: false, - onStrike: true, - nonStrike: true, - }, - ], - bowlers: [ - { - name: 'Mohammed Shami', - overs: 9, - maidens: 0, - runs: 74, - wickets: 1, - balls: 55, - }, - { - name: 'Hardik Pandya', - overs: 3, - maidens: 0, - runs: 30, - wickets: 0, - balls: 18, - }, - { - name: 'Varun Chakaravarthy', - overs: 10, - maidens: 0, - runs: 45, - wickets: 2, - balls: 60, - }, - { - name: 'Kuldeep Yadav', - overs: 10, - maidens: 0, - runs: 40, - wickets: 2, - balls: 60, - }, - { - name: 'Axar Patel', - overs: 8, - maidens: 0, - runs: 29, - wickets: 0, - balls: 48, - }, - { - name: 'Ravindra Jadeja', - overs: 10, - maidens: 0, - runs: 30, - wickets: 1, - balls: 60, - }, - ], - fallOfWickets: [ - { - order: 1, - name: 'Shubman Gill', - runs: 105, - }, - { - order: 2, - name: 'Virat Kohli', - runs: 106, - }, - { - order: 3, - name: 'Rohit Sharma', - runs: 122, - }, - ], - }, - { - description: 'New Zealand first innings', - battingTeam: 'New Zealand', - inningsTotals: { - runs: 254, - overs: '49.0', - extras: 8, - wickets: 7, - }, - extras: { - byes: 0, - legByes: 3, - noBalls: 0, - penalties: 0, - wides: 13, - }, - batters: [ - { - name: 'Will Young', - ballsFaced: 23, - runs: 15, - fours: 2, - sixes: 0, - howOut: 'lbw b Vinod', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Rachin Ravindra', - ballsFaced: 29, - runs: 37, - fours: 4, - sixes: 1, - howOut: 'b Yadav', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Kane Williamson', - ballsFaced: 14, - runs: 11, - fours: 1, - sixes: 0, - howOut: 'c & b Yadav', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Daryl Mitchell', - ballsFaced: 101, - runs: 63, - fours: 3, - sixes: 0, - howOut: 'c Sharma b Ahmed', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Tom Latham', - ballsFaced: 56, - runs: 44, - fours: 4, - sixes: 0, - howOut: 'c Kohli b Shami', - out: true, - onStrike: false, - nonStrike: true, - }, - { - name: 'Glenn Phillips', - ballsFaced: 34, - runs: 55, - fours: 3, - sixes: 3, - howOut: 'not out', - out: false, - onStrike: true, - nonStrike: true, - }, - { - name: 'Michael Bracewell', - ballsFaced: 8, - runs: 5, - fours: 0, - sixes: 0, - howOut: 'not out', - out: false, - onStrike: false, - nonStrike: true, - }, - ], - bowlers: [ - { - name: 'Mohammed Shami', - overs: 9, - maidens: 0, - runs: 74, - wickets: 1, - balls: 54, - }, - { - name: 'Hardik Pandya', - overs: 3, - maidens: 0, - runs: 30, - wickets: 0, - balls: 18, - }, - { - name: 'Varun Chakaravarthy', - overs: 10, - maidens: 0, - runs: 45, - wickets: 2, - balls: 60, - }, - { - name: 'Kuldeep Yadav', - overs: 10, - maidens: 0, - runs: 40, - wickets: 2, - balls: 60, - }, - ], - fallOfWickets: [ - { - order: 1, - name: 'Will Young', - runs: 57, - }, - { - order: 2, - name: 'Rachin Ravindra', - runs: 69, - }, - ], - }, - ], - }, -}); diff --git a/dotcom-rendering/src/components/CricketScorecardPageNew.tsx b/dotcom-rendering/src/components/CricketScorecardPageNew.tsx deleted file mode 100644 index 86cb51f06c0..00000000000 --- a/dotcom-rendering/src/components/CricketScorecardPageNew.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { css } from '@emotion/react'; -import { from, space } from '@guardian/source/foundations'; -import type { ComponentProps } from 'react'; -import type { CricketMatch, Innings } from '../cricketMatchV2'; -import { grid } from '../grid'; -import { type EditionId } from '../lib/edition'; -import { palette } from '../palette'; -import { CricketMatchHeader } from './CricketMatchHeader/CricketMatchHeader'; -import { CricketScorecardNew } from './CricketScorecardNew'; -import type { Tabs } from './FootballMatchHeader/Tabs'; - -export const CricketScorecardPageNew = ({ - match, - allInnings, - edition, - lineups, - officials, - tabs, -}: { - match: CricketMatch; - allInnings: Innings[]; - edition: EditionId; - lineups: { - homeTeam: string[]; - awayTeam: string[]; - }; - officials: string[]; - tabs: ComponentProps; -}) => { - return ( -
    - -
    -
    - -
    -
    -
    - ); -}; - -const bodyGridStyles = css` - ${grid.paddedContainer} - position: relative; - padding-top: ${space[4]}px; - padding-bottom: ${space[8]}px; - ${from.tablet} { - padding-top: ${space[2]}px; - padding-bottom: ${space[14]}px; - &::before, - &::after { - content: ''; - position: absolute; - border-left: 1px solid ${palette('--article-border')}; - z-index: 1; - top: 0; - bottom: 0; - } - - &::after { - right: 0; - } - } -`; diff --git a/dotcom-rendering/src/components/CricketScorecardTab.tsx b/dotcom-rendering/src/components/CricketScorecardTab.tsx new file mode 100644 index 00000000000..f59cab04306 --- /dev/null +++ b/dotcom-rendering/src/components/CricketScorecardTab.tsx @@ -0,0 +1,85 @@ +import { css } from '@emotion/react'; +import { from, space } from '@guardian/source/foundations'; +import { useEffect, useRef } from 'react'; +import type { CricketMatch } from '../cricketMatchV2'; +import { grid } from '../grid'; +import { palette } from '../palette'; +import { CricketScorecardNew } from './CricketScorecardNew'; + +export type CricketScorecardTabProps = Pick< + CricketMatch, + 'innings' | 'officials' | 'homeTeam' | 'awayTeam' | 'result' +>; + +// this is needed to override global style +// html:not(.src-focus-disabled) *:focus +const disableFocusShadow = css({ + '&&:focus': { + boxShadow: 'none', + }, +}); + +export const CricketScorecardTab = ({ + innings, + officials, + homeTeam, + awayTeam, + result, +}: CricketScorecardTabProps) => { + const regionRef = useRef(null); + + useEffect(() => { + // Focus the tab content when it renders for accessibility, so that screen readers will read out the scorecard label when it displays. + regionRef.current?.focus({ + preventScroll: true, + }); + }, []); + + return ( +
    +
    + +
    +
    + ); +}; + +const bodyGridStyles = css` + ${grid.paddedContainer} + position: relative; + padding-top: ${space[4]}px; + padding-bottom: ${space[8]}px; + ${from.tablet} { + padding-top: ${space[2]}px; + padding-bottom: ${space[14]}px; + &::before, + &::after { + content: ''; + position: absolute; + border-left: 1px solid ${palette('--article-border')}; + z-index: 1; + top: 0; + bottom: 0; + } + + &::after { + right: 0; + } + } +`; diff --git a/dotcom-rendering/src/components/CricketScorecardTabRemoteRender.tsx b/dotcom-rendering/src/components/CricketScorecardTabRemoteRender.tsx new file mode 100644 index 00000000000..87eb5349d01 --- /dev/null +++ b/dotcom-rendering/src/components/CricketScorecardTabRemoteRender.tsx @@ -0,0 +1,54 @@ +import { useEffect, useRef } from 'react'; +import type { Root } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; +import type { CricketScorecardTabProps } from './CricketScorecardTab'; +import { CricketScorecardTab } from './CricketScorecardTab'; + +type Props = { + tabContentElement?: HTMLElement; +} & CricketScorecardTabProps; + +/** + * Renders the cricket scorecard page into another part of the DOM. + * + * First of all, only this tab is rendered client side because adding a cricket + * scorecard page to the app and MAPI would be a significant amount of work + * across apps & webx. + * + * Secondly, we use ReactDOM's `createRoot` and `render` here instead of + * rendering as part of the main React tree to avoid wrapping a large portion + * of the StandardLayout and LiveLayout in an island, which would be very large and + * require passing the entire live blog or article content as props to said island. + * + */ +export const CricketScorecardTabRemoteRender = ({ + tabContentElement, + innings, + officials, + homeTeam, + awayTeam, + result, +}: Props) => { + const rootRef = useRef(null); + + useEffect(() => { + if (tabContentElement === undefined) { + return; + } + // Create the root if it doesn't exist, subsequent renders will reuse the same root + rootRef.current ??= createRoot(tabContentElement); + + // `render` will efficiently update the content as needed for subsequent renders, we don't need to unmount or clear the root ourselves + rootRef.current.render( + , + ); + }, [tabContentElement, innings, officials, homeTeam, awayTeam, result]); + + return null; +}; diff --git a/dotcom-rendering/src/components/DecideContainer.tsx b/dotcom-rendering/src/components/DecideContainer.tsx index 34621c7360f..af561f32e63 100644 --- a/dotcom-rendering/src/components/DecideContainer.tsx +++ b/dotcom-rendering/src/components/DecideContainer.tsx @@ -31,7 +31,6 @@ type Props = { frontId?: string; collectionId: number; containerLevel?: DCRContainerLevel; - isNewsletterSignupCardEnabled?: boolean; }; export const DecideContainer = ({ @@ -47,7 +46,6 @@ export const DecideContainer = ({ frontId, collectionId, containerLevel, - isNewsletterSignupCardEnabled = false, }: Props) => { switch (containerType) { case 'nav/list': @@ -57,13 +55,7 @@ export const DecideContainer = ({ case 'scrollable/highlights': return ( - + ); case 'flexible/special': diff --git a/dotcom-rendering/src/components/DirectoryPageNav.island.tsx b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx index c661f97f8be..350a0c1a78f 100644 --- a/dotcom-rendering/src/components/DirectoryPageNav.island.tsx +++ b/dotcom-rendering/src/components/DirectoryPageNav.island.tsx @@ -86,7 +86,7 @@ const worldCup2026Links = [ id: 'football/ng-interactive/2026/jun/04/bracketology-predict-a-path-to-world-cup-victory', }, { - label: 'Golden boot', + label: 'Golden Boot', id: 'football/ng-interactive/2026/jun/04/golden-boot-world-cup-2026-top-goalscorers-winner', }, { @@ -304,12 +304,7 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }); const navWeb = css({ - '&': css( - grid.paddedContainer, - grid.verticalRules({ - color: borderColor, - }), - ), + '&': css(grid.paddedContainer, grid.outerRules(borderColor)), }); const largeLinkStyles = css({ @@ -438,8 +433,9 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { }, }); - const boldSmallLink = css({ + const selectedLink = css({ ...textSansBold14Object, + pointerEvents: 'none', }); return ( @@ -471,7 +467,7 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { smallLink, primaryLinkStyles, !isApps && primaryLinkHoverStyles, - pageId === config.title.id && boldSmallLink, + pageId === config.title.id && selectedLink, ]} > {config.titleIcon} @@ -486,7 +482,7 @@ export const DirectoryPageNav = ({ pageId, pageTags }: Props) => { css={[ smallLink, !isApps && smallLinkWeb, - pageId === link.id && boldSmallLink, + pageId === link.id && selectedLink, ]} > {link.label} diff --git a/dotcom-rendering/src/components/Discussion.tsx b/dotcom-rendering/src/components/Discussion.tsx index 195662fb77d..63251f9f8f8 100644 --- a/dotcom-rendering/src/components/Discussion.tsx +++ b/dotcom-rendering/src/components/Discussion.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import { isUndefined, storage } from '@guardian/libs'; import { space } from '@guardian/source/foundations'; -import { SvgPlus } from '@guardian/source/react-components'; +import { SvgChevronDownSingle } from '@guardian/source/react-components'; import { useEffect, useReducer } from 'react'; import { assertUnreachable } from '../lib/assert-unreachable'; import type { @@ -536,7 +536,7 @@ export const Discussion = ({ onClick={() => { dispatch({ type: 'expandComments' }); }} - icon={} + icon={} linkName="view-more-comments" > View more comments diff --git a/dotcom-rendering/src/components/Discussion/CommentContainer.tsx b/dotcom-rendering/src/components/Discussion/CommentContainer.tsx index 02cd0f3bdb1..12774aac95e 100644 --- a/dotcom-rendering/src/components/Discussion/CommentContainer.tsx +++ b/dotcom-rendering/src/components/Discussion/CommentContainer.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import { space } from '@guardian/source/foundations'; -import { SvgPlus } from '@guardian/source/react-components'; +import { SvgChevronDownSingle } from '@guardian/source/react-components'; import { useState } from 'react'; import type { CommentType, @@ -199,7 +199,7 @@ export const CommentContainer = ({ > } + icon={} iconSide="left" linkName="Show more replies" onClick={() => expand(comment.id)} diff --git a/dotcom-rendering/src/components/EmailSignUpWrapper.island.test.tsx b/dotcom-rendering/src/components/EmailSignUpWrapper.island.test.tsx index 703d64fc59b..6dfed7691ea 100644 --- a/dotcom-rendering/src/components/EmailSignUpWrapper.island.test.tsx +++ b/dotcom-rendering/src/components/EmailSignUpWrapper.island.test.tsx @@ -1,8 +1,7 @@ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import { submitComponentEvent } from '../client/ophan/ophan'; -import { AB_TEST_NAME } from '../lib/newsletterSignupTracking'; -import { useAB } from '../lib/useAB'; +import { NEWSLETTER_SIGNUP_COMPONENT_ID } from '../lib/newsletterSignupTracking'; import { useIsSignedIn } from '../lib/useAuthStatus'; import { useNewsletterSubscription } from '../lib/useNewsletterSubscription'; import { ConfigProvider } from './ConfigContext'; @@ -12,10 +11,6 @@ jest.mock('../client/ophan/ophan', () => ({ submitComponentEvent: jest.fn(() => Promise.resolve()), })); -jest.mock('../lib/useAB', () => ({ - useAB: jest.fn(), -})); - jest.mock('../lib/useAuthStatus', () => ({ useIsSignedIn: jest.fn(), })); @@ -35,10 +30,6 @@ jest.mock('./NewsletterSignupForm.island', () => ({ ), })); -jest.mock('./SecureSignup.island', () => ({ - SecureSignup: () =>
    SecureSignup
    , -})); - jest.mock('./NewsletterSignupCardContainer', () => ({ NewsletterSignupCardContainer: ({ children, @@ -51,12 +42,6 @@ jest.mock('./NewsletterSignupCardContainer', () => ({ ), })); -jest.mock('./EmailSignup', () => ({ - EmailSignup: ({ children }: { children: React.ReactNode }) => ( -
    {children}
    - ), -})); - const defaultProps = { index: 0, listId: 4147, @@ -64,7 +49,6 @@ const defaultProps = { name: 'The Recap', description: 'A weekly sports roundup', frequency: 'Weekly', - successDescription: "We'll send you The Recap every week", theme: 'sport', idApiUrl: 'https://idapi.theguardian.com', }; @@ -83,208 +67,52 @@ const renderWrapper = (props = {}, renderingTarget: 'Web' | 'Apps' = 'Web') => , ); -const mockAbTests = ( - variant: 'control' | 'variantIllustratedCard' | 'variantNewField', -) => { - (useAB as jest.Mock).mockReturnValue({ - getParticipations: () => ({ - [AB_TEST_NAME]: variant, - }), - }); -}; - describe('EmailSignUpWrapper', () => { beforeEach(() => { jest.resetAllMocks(); - // Default: AB API not yet hydrated - (useAB as jest.Mock).mockReturnValue(undefined); (useIsSignedIn as jest.Mock).mockReturnValue(false); (useNewsletterSubscription as jest.Mock).mockReturnValue(false); }); - describe('flag off (showNewNewsletterSignupCard = false)', () => { - it('shows a placeholder while subscription status is loading', () => { - (useNewsletterSubscription as jest.Mock).mockReturnValue(undefined); - renderWrapper(); - // The Placeholder component renders a div with specific heights - expect( - screen.queryByTestId('email-signup'), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('newsletter-signup-card-container'), - ).not.toBeInTheDocument(); - }); - - it('renders nothing when the user is already subscribed', () => { - mockAbTests('control'); - (useNewsletterSubscription as jest.Mock).mockReturnValue(true); - const { container } = renderWrapper({ - hideNewsletterSignupComponentForSubscribers: true, - }); - expect(container).toBeEmptyDOMElement(); - }); - - it('renders the legacy EmailSignup when the user is not subscribed', () => { - mockAbTests('control'); + describe('rendering', () => { + it('renders the NewsletterSignupCardContainer', () => { renderWrapper(); - expect(screen.getByTestId('email-signup')).toBeInTheDocument(); - expect(screen.getByTestId('secure-signup')).toBeInTheDocument(); - expect( - screen.queryByTestId('newsletter-signup-card-container'), - ).not.toBeInTheDocument(); - }); - }); - - describe('flag on (showNewNewsletterSignupCard = true)', () => { - it('shows a placeholder when AB API has not hydrated yet', () => { - // useAB returns undefined before hydration — component shows - // Placeholder rather than committing to control or variant early. - // (default set in outer beforeEach, no override needed) - renderWrapper({ showNewNewsletterSignupCard: true }); - expect( - screen.queryByTestId('email-signup'), - ).not.toBeInTheDocument(); - expect( - screen.queryByTestId('newsletter-signup-card-container'), - ).not.toBeInTheDocument(); - }); - - it('renders the legacy EmailSignup for users in the control group', () => { - mockAbTests('control'); - renderWrapper({ showNewNewsletterSignupCard: true }); - expect(screen.getByTestId('email-signup')).toBeInTheDocument(); - expect( - screen.queryByTestId('newsletter-signup-card-container'), - ).not.toBeInTheDocument(); - }); - - it('renders NewsletterSignupCardContainer for users in the variant group', () => { - mockAbTests('variantIllustratedCard'); - renderWrapper({ showNewNewsletterSignupCard: true }); expect( screen.getByTestId('newsletter-signup-card-container'), ).toBeInTheDocument(); expect( screen.getByTestId('newsletter-signup-form'), ).toBeInTheDocument(); - expect( - screen.queryByTestId('email-signup'), - ).not.toBeInTheDocument(); }); - it('renders the legacy EmailSignup for users in the variantNewField group', () => { - mockAbTests('variantNewField'); - renderWrapper({ showNewNewsletterSignupCard: true }); - expect(screen.getByTestId('email-signup')).toBeInTheDocument(); - expect(screen.getByTestId('secure-signup')).toBeInTheDocument(); + it('renders the NewsletterSignupCardContainer on Apps as well as Web', () => { + renderWrapper({}, 'Apps'); expect( - screen.queryByTestId('newsletter-signup-card-container'), - ).not.toBeInTheDocument(); + screen.getByTestId('newsletter-signup-card-container'), + ).toBeInTheDocument(); }); - it('does not hide the variant for already-subscribed users (subscription handled inside the card)', () => { - mockAbTests('variantIllustratedCard'); + it('still renders the card when the user is already subscribed', () => { (useNewsletterSubscription as jest.Mock).mockReturnValue(true); - renderWrapper({ - showNewNewsletterSignupCard: true, - hideNewsletterSignupComponentForSubscribers: true, - }); + renderWrapper(); expect( screen.getByTestId('newsletter-signup-card-container'), ).toBeInTheDocument(); }); }); - describe('Apps rendering target', () => { - it('renders the legacy EmailSignup without waiting for AB to resolve', () => { - // useAB remains undefined (AB framework never initialises on Apps) - // but the component should not block on it - renderWrapper({ showNewNewsletterSignupCard: true }, 'Apps'); - expect(screen.getByTestId('email-signup')).toBeInTheDocument(); - expect( - screen.queryByTestId('newsletter-signup-card-container'), - ).not.toBeInTheDocument(); - }); - - it('fires a VIEW tracking event without waiting for AB to resolve', () => { - renderWrapper({ showNewNewsletterSignupCard: true }, 'Apps'); - expect(submitComponentEvent).toHaveBeenCalledWith( - expect.objectContaining({ - action: 'VIEW', - abTest: { name: AB_TEST_NAME, variant: 'control' }, - }), - 'Apps', - ); - }); - }); - describe('VIEW tracking', () => { - it('fires a VIEW event with the correct control component id and ab metadata', () => { - mockAbTests('control'); - renderWrapper({ showNewNewsletterSignupCard: true }); - - expect(submitComponentEvent).toHaveBeenCalledWith( - expect.objectContaining({ - component: { - componentType: 'NEWSLETTER_SUBSCRIPTION', - id: 'AR SecureSignup the-recap', - }, - action: 'VIEW', - abTest: { name: AB_TEST_NAME, variant: 'control' }, - }), - 'Web', - ); - }); - - it('fires a VIEW event with the correct variant component id and ab metadata', () => { - mockAbTests('variantIllustratedCard'); - renderWrapper({ showNewNewsletterSignupCard: true }); - - expect(submitComponentEvent).toHaveBeenCalledWith( - expect.objectContaining({ - component: { - componentType: 'NEWSLETTER_SUBSCRIPTION', - id: 'AR NewsletterSignupForm the-recap - variantIllustratedCard', - }, - action: 'VIEW', - abTest: { - name: AB_TEST_NAME, - variant: 'variantIllustratedCard', - }, - }), - 'Web', - ); - }); - - it('fires a VIEW event with the correct variantNewField component id and ab metadata', () => { - mockAbTests('variantNewField'); - renderWrapper({ showNewNewsletterSignupCard: true }); + it('fires a VIEW event with the in-article signup form component id', () => { + renderWrapper(); expect(submitComponentEvent).toHaveBeenCalledWith( expect.objectContaining({ component: { componentType: 'NEWSLETTER_SUBSCRIPTION', - id: 'AR SecureSignup the-recap - variantNewField', - }, - action: 'VIEW', - abTest: { - name: AB_TEST_NAME, - variant: 'variantNewField', + id: NEWSLETTER_SIGNUP_COMPONENT_ID.inArticleSignupForm( + 'the-recap', + ), }, - }), - 'Web', - ); - }); - - it('fires a VIEW event with the control component id when the flag is off', () => { - mockAbTests('control'); - renderWrapper({ showNewNewsletterSignupCard: false }); - - expect(submitComponentEvent).toHaveBeenCalledWith( - expect.objectContaining({ - component: expect.objectContaining({ - id: 'AR SecureSignup the-recap', - }), action: 'VIEW', }), 'Web', @@ -292,45 +120,21 @@ describe('EmailSignUpWrapper', () => { }); it('fires the VIEW event exactly once on mount', () => { - mockAbTests('control'); - renderWrapper({ showNewNewsletterSignupCard: true }); + renderWrapper(); expect(submitComponentEvent).toHaveBeenCalledTimes(1); }); it('does not fire a VIEW event while subscription status is loading', () => { - mockAbTests('control'); (useNewsletterSubscription as jest.Mock).mockReturnValue(undefined); - renderWrapper({ showNewNewsletterSignupCard: true }); - - expect(submitComponentEvent).not.toHaveBeenCalled(); - }); - - it('does not fire a VIEW event while the AB client has not hydrated', () => { - // useAB returns undefined — default set in beforeEach - renderWrapper({ showNewNewsletterSignupCard: true }); - - expect(submitComponentEvent).not.toHaveBeenCalled(); - }); - - it('does not fire a VIEW event for control users who are already subscribed', () => { - mockAbTests('control'); - (useNewsletterSubscription as jest.Mock).mockReturnValue(true); - renderWrapper({ - showNewNewsletterSignupCard: true, - hideNewsletterSignupComponentForSubscribers: true, - }); + renderWrapper(); expect(submitComponentEvent).not.toHaveBeenCalled(); }); - it('does not fire a VIEW event for variant users who are already subscribed', () => { - mockAbTests('variantIllustratedCard'); + it('does not fire a VIEW event when the user is already subscribed', () => { (useNewsletterSubscription as jest.Mock).mockReturnValue(true); - renderWrapper({ - showNewNewsletterSignupCard: true, - hideNewsletterSignupComponentForSubscribers: true, - }); + renderWrapper(); expect(submitComponentEvent).not.toHaveBeenCalled(); }); diff --git a/dotcom-rendering/src/components/EmailSignUpWrapper.island.tsx b/dotcom-rendering/src/components/EmailSignUpWrapper.island.tsx index 37eecd30250..84b203d8dbf 100644 --- a/dotcom-rendering/src/components/EmailSignUpWrapper.island.tsx +++ b/dotcom-rendering/src/components/EmailSignUpWrapper.island.tsx @@ -1,60 +1,36 @@ -import type { Breakpoint } from '@guardian/source/foundations'; import { useEffect, useRef } from 'react'; import { - AB_TEST_NAME, NEWSLETTER_SIGNUP_COMPONENT_ID, sendNewsletterSignupEvent, } from '../lib/newsletterSignupTracking'; -import { useAB } from '../lib/useAB'; import { useIsSignedIn } from '../lib/useAuthStatus'; import { useNewsletterSubscription } from '../lib/useNewsletterSubscription'; import { useConfig } from './ConfigContext'; import type { EmailSignUpProps } from './EmailSignup'; -import { EmailSignup } from './EmailSignup'; import { InlineSkipToWrapper } from './InlineSkipToWrapper'; import { Island } from './Island'; -import { NewsletterPrivacyMessage } from './NewsletterPrivacyMessage'; import { NewsletterSignupCardContainer } from './NewsletterSignupCardContainer'; import { NewsletterSignupForm } from './NewsletterSignupForm.island'; -import { Placeholder } from './Placeholder'; -import { SecureSignup } from './SecureSignup.island'; - -/** - * Approximate heights of the EmailSignup component at different breakpoints. - */ -const PLACEHOLDER_HEIGHTS = new Map([ - ['mobile', 220], - ['tablet', 180], - ['desktop', 180], -]); +// When the next A/B experiment is added (e.g. preview-button test), import +// useAB and AB_TEST_NAME from their respective modules and thread them through +// sendNewsletterSignupEvent's `abTest` param and NewsletterSignupForm's `abTest` prop. interface EmailSignUpWrapperProps extends EmailSignUpProps { index: number; listId: number; identityName: string; category?: string; - successDescription: string; - /** Illustration image URL (square crop) for the NewsletterSignupCard variant */ + /** Illustration image URL (square crop) for the NewsletterSignupCard */ illustrationSquare?: string; idApiUrl: string; exampleUrl?: string; - /** You should only set this to true if the privacy message will be shown elsewhere on the page */ - hidePrivacyMessage?: boolean; - /** Feature flag to enable hiding newsletter signup for already subscribed users */ - hideNewsletterSignupComponentForSubscribers?: boolean; - /** Feature flag to show the new NewsletterSignupCard design instead of EmailSignup */ - showNewNewsletterSignupCard?: boolean; } /** * EmailSignUpWrapper as an island component. * * This component needs to be hydrated client-side because it uses - * the useNewsletterSubscription hook which depends on auth status - * to determine if the user is already subscribed to the newsletter. - * - * If the user is signed in and already subscribed, this component - * will return null (hide the signup form). + * client-side hooks for auth status and tracking. */ export const EmailSignUpWrapper = ({ index, @@ -68,57 +44,26 @@ export const EmailSignUpWrapper = ({ illustrationSquare, frequency, theme, - successDescription, - hidePrivacyMessage, - hideNewsletterSignupComponentForSubscribers = false, - showNewNewsletterSignupCard = false, }: EmailSignUpWrapperProps) => { const { renderingTarget } = useConfig(); - - const abTestEnabled = - showNewNewsletterSignupCard && renderingTarget === 'Web'; - - const abTests = useAB(); - const abResolved = abTests !== undefined; - - const getVariantName = () => { - const currentUserVariant = abTests?.getParticipations()[AB_TEST_NAME]; - if ( - currentUserVariant && - ['variantNewField', 'variantIllustratedCard'].includes( - currentUserVariant, - ) - ) { - return currentUserVariant; - } - return 'control'; - }; - const abVariant = abTestEnabled ? getVariantName() : 'control'; - - const isSubscribed = useNewsletterSubscription( - listId, - idApiUrl, - hideNewsletterSignupComponentForSubscribers, - ); const isSignedIn = useIsSignedIn(); + const isSubscribed = useNewsletterSubscription(listId, idApiUrl); + + const componentId = + NEWSLETTER_SIGNUP_COMPONENT_ID.inArticleSignupForm(identityName); const viewFiredRef = useRef(false); useEffect(() => { - if (abTestEnabled && !abResolved) { - return; - } - // Wait for subscription status in both branches — we only want to track - // a view of the actual signup form, not a loading state or success message. + // Wait for subscription status — we only want to track a view of the + // actual signup form, not a loading state or success message. if (isSubscribed === undefined) { return; } - // Don't fire if the user is already subscribed: in both branches they - // will see a success/already-subscribed message, not the signup form. + // Don't fire if the user is already subscribed. if (isSubscribed) { return; } - // Guard against double-firing (e.g. if deps change after the first fire) if (viewFiredRef.current) { return; } @@ -126,111 +71,44 @@ export const EmailSignUpWrapper = ({ sendNewsletterSignupEvent({ action: 'VIEW', identityName, - componentId: - abVariant === 'variantIllustratedCard' - ? NEWSLETTER_SIGNUP_COMPONENT_ID.variantIllustratedCard( - identityName, - ) - : abVariant === 'variantNewField' - ? NEWSLETTER_SIGNUP_COMPONENT_ID.variantNewField( - identityName, - ) - : NEWSLETTER_SIGNUP_COMPONENT_ID.control(identityName), + componentId, renderingTarget, value: { eventDescription: 'newsletter-signup-viewed', }, - // Use the standard Ophan abTest field so Ophan can join events - // to the A/B test — not strings encoded inside value. - abTest: { name: AB_TEST_NAME, variant: abVariant }, }); - }, [ - abResolved, - abVariant, - identityName, - isSubscribed, - abTestEnabled, - renderingTarget, - ]); - - if ((abTestEnabled && !abResolved) || isSubscribed === undefined) { - return ; - } - - if (abVariant === 'variantIllustratedCard') { - return ( - - - {(previewAction) => ( - - - - )} - - - ); - } - - // Don't render control if user is already subscribed - if (isSubscribed) { - return null; - } - - const emailInputName = - abVariant === 'variantNewField' ? 'emailAddress' : 'email'; - const emailInputId = - abVariant === 'variantNewField' - ? `emailInput-${identityName}` - : undefined; + }, [componentId, identityName, isSubscribed, renderingTarget]); return ( - - - - - {!hidePrivacyMessage && } - + {(previewAction) => ( + + + + )} + ); }; diff --git a/dotcom-rendering/src/components/EmailSignUpWrapper.stories.tsx b/dotcom-rendering/src/components/EmailSignUpWrapper.stories.tsx index 39745f04dfc..a3e92e981dd 100644 --- a/dotcom-rendering/src/components/EmailSignUpWrapper.stories.tsx +++ b/dotcom-rendering/src/components/EmailSignUpWrapper.stories.tsx @@ -2,30 +2,10 @@ import type { StoryObj } from '@storybook/react-webpack5'; import { mocked, within } from 'storybook/test'; import preview from '../../.storybook/preview'; import { lazyFetchEmailWithTimeout } from '../lib/fetchEmail'; -import { AB_TEST_NAME } from '../lib/newsletterSignupTracking'; -import { useAB } from '../lib/useAB'; import { useIsSignedIn } from '../lib/useAuthStatus'; import { useNewsletterSubscription } from '../lib/useNewsletterSubscription'; import { EmailSignUpWrapper } from './EmailSignUpWrapper.island'; -/** Resolves `useAB` as if the AB framework has hydrated, placing the user in control or variant. */ -const mockAB = ( - variant: 'control' | 'variantNewField' | 'variantIllustratedCard', -) => { - mocked(useAB).mockReturnValue({ - isUserInTestGroup: (_testName: string, group: string) => - group === variant, - isUserInTest: () => true, - getParticipations: () => - (variant !== 'control' - ? { - [AB_TEST_NAME]: variant, - } - : {}) as Record, - trackABTests: () => ({}), - }); -}; - const meta = preview.meta({ title: 'Components/EmailSignUpWrapper', component: EmailSignUpWrapper, @@ -41,44 +21,16 @@ const defaultArgs = { "The best of our sports journalism from the past seven days and a heads-up on the weekend's action", name: 'The Recap', frequency: 'Weekly', - successDescription: "We'll send you The Recap every week", theme: 'sport', idApiUrl: 'https://idapi.theguardian.com', exampleUrl: 'https://www.theguardian.com/email/the-recap', -} satisfies Story['args']; - -const newCardArgs = { - ...defaultArgs, - showNewNewsletterSignupCard: true, - hideNewsletterSignupComponentForSubscribers: true, illustrationSquare: 'https://i.guim.co.uk/img/uploads/2023/11/01/SaturdayEdition_-_5-3.jpg?width=220&dpr=2&s=none&crop=5%3A3', } satisfies Story['args']; -export const Placeholder = meta.story({ - args: { hidePrivacyMessage: false, ...defaultArgs }, - beforeEach() { - mockAB('control'); - mocked(useNewsletterSubscription).mockReturnValue(undefined); - }, -}); - -export const DefaultStory = meta.story({ - args: { hidePrivacyMessage: true, ...defaultArgs }, - beforeEach() { - mockAB('control'); - mocked(useNewsletterSubscription).mockReturnValue(false); - mocked(useIsSignedIn).mockReturnValue(false); - mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => - Promise.resolve(null), - ); - }, -}); - -export const DefaultStoryWithPrivacy = meta.story({ - args: { hidePrivacyMessage: false, ...defaultArgs }, +export const SignedOut = meta.story({ + args: { ...defaultArgs }, beforeEach() { - mockAB('control'); mocked(useNewsletterSubscription).mockReturnValue(false); mocked(useIsSignedIn).mockReturnValue(false); mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => @@ -87,10 +39,9 @@ export const DefaultStoryWithPrivacy = meta.story({ }, }); -export const SignedInNotSubscribed = meta.story({ - args: { hidePrivacyMessage: false, ...defaultArgs }, +export const SignedIn = meta.story({ + args: { ...defaultArgs }, beforeEach() { - mockAB('control'); mocked(useNewsletterSubscription).mockReturnValue(false); mocked(useIsSignedIn).mockReturnValue(true); mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => @@ -100,68 +51,8 @@ export const SignedInNotSubscribed = meta.story({ }); export const SignedInAlreadySubscribed = meta.story({ - args: { - hidePrivacyMessage: false, - ...defaultArgs, - hideNewsletterSignupComponentForSubscribers: true, - }, - beforeEach() { - mockAB('control'); - mocked(useNewsletterSubscription).mockReturnValue(true); - }, -}); - -export const FeatureFlagDisabled = meta.story({ - args: { - hidePrivacyMessage: false, - ...defaultArgs, - hideNewsletterSignupComponentForSubscribers: false, - }, - beforeEach() { - mockAB('control'); - mocked(useNewsletterSubscription).mockReturnValue(false); - mocked(useIsSignedIn).mockReturnValue(true); - mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => - Promise.resolve('test@example.com'), - ); - }, - parameters: { - docs: { - description: { - story: 'When the hideNewsletterSignupComponentForSubscribers feature flag is disabled, the signup form is always shown regardless of subscription status.', - }, - }, - }, -}); - -export const NewsletterSignupCardSignedInNotSubscribed = meta.story({ - args: newCardArgs, - beforeEach() { - mockAB('variantIllustratedCard'); - mocked(useNewsletterSubscription).mockReturnValue(false); - mocked(useIsSignedIn).mockReturnValue(true); - mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => - Promise.resolve('test@example.com'), - ); - }, -}); - -export const NewsletterSignupCardSignedOutNotSubscribed = meta.story({ - args: newCardArgs, - beforeEach() { - mockAB('variantIllustratedCard'); - mocked(useNewsletterSubscription).mockReturnValue(false); - mocked(useIsSignedIn).mockReturnValue(false); - mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => - Promise.resolve(null), - ); - }, -}); - -export const NewsletterSignupCardSignedInAlreadySubscribed = meta.story({ - args: newCardArgs, + args: { ...defaultArgs }, beforeEach() { - mockAB('variantIllustratedCard'); mocked(useNewsletterSubscription).mockReturnValue(true); mocked(useIsSignedIn).mockReturnValue(true); mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => @@ -170,10 +61,9 @@ export const NewsletterSignupCardSignedInAlreadySubscribed = meta.story({ }, }); -export const NewsletterSignupCardSignedOutAlreadySubscribed = meta.story({ - args: newCardArgs, +export const SignedOutAlreadySubscribed = meta.story({ + args: { ...defaultArgs }, beforeEach() { - mockAB('variantIllustratedCard'); mocked(useNewsletterSubscription).mockReturnValue(true); mocked(useIsSignedIn).mockReturnValue(false); mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => @@ -182,10 +72,9 @@ export const NewsletterSignupCardSignedOutAlreadySubscribed = meta.story({ }, }); -export const NewsletterSignupCardFocused = meta.story({ - args: newCardArgs, +export const EmailInputFocused = meta.story({ + args: { ...defaultArgs }, beforeEach() { - mockAB('variantIllustratedCard'); mocked(useNewsletterSubscription).mockReturnValue(false); mocked(useIsSignedIn).mockReturnValue(false); mocked(lazyFetchEmailWithTimeout).mockReturnValue(() => diff --git a/dotcom-rendering/src/components/FeastContextualNudge.island.tsx b/dotcom-rendering/src/components/FeastContextualNudge.island.tsx new file mode 100644 index 00000000000..bfee9dbd539 --- /dev/null +++ b/dotcom-rendering/src/components/FeastContextualNudge.island.tsx @@ -0,0 +1,225 @@ +import { css } from '@emotion/react'; +import { + article15, + from, + palette as sourcePalette, + space, +} from '@guardian/source/foundations'; +import { LinkButton } from '@guardian/source/react-components'; +import { useEffect, useState } from 'react'; +import { useAB } from '../lib/useAB'; +import type { StageType } from '../types/config'; +import type { RecipeBlockElement } from '../types/content'; +import { useConfig } from './ConfigContext'; + +// ── Feast brand colours ─────────────────────────────────────────────────────── + +const FEAST_BG = '#F3F3E9'; +const FEAST_BG_DARK = '#2B2B26'; +const FEAST_TEXT = sourcePalette.neutral[10]; +const FEAST_TEXT_DARK = sourcePalette.neutral[100]; +const FEAST_SUBTEXT = sourcePalette.neutral[20]; +const FEAST_SUBTEXT_DARK = sourcePalette.neutral[93]; +const FEAST_GREEN = '#68773C'; +const FEAST_GREEN_HOVER = '#4d5c2b'; +const FEAST_BORDER = FEAST_GREEN; +const FEAST_BORDER_DARK = FEAST_GREEN; + +// ── CSS custom properties ───────────────────────────────────────────────────── + +const lightVars = css` + --feast-nudge-bg: ${FEAST_BG}; + --feast-nudge-heading: ${FEAST_TEXT}; + --feast-nudge-subtext: ${FEAST_SUBTEXT}; + --feast-nudge-border: ${FEAST_BORDER}; +`; + +const darkVars = css` + --feast-nudge-bg: ${FEAST_BG_DARK}; + --feast-nudge-heading: ${FEAST_TEXT_DARK}; + --feast-nudge-subtext: ${FEAST_SUBTEXT_DARK}; + --feast-nudge-border: ${FEAST_BORDER_DARK}; +`; + +// ── Deep-link helpers ───────────────────────────────────────────────────────── + +const FEAST_ADJUST_TOKEN_PROD = '20wmhy68'; +const FEAST_ADJUST_TOKEN_CODE = '20o7ykck'; + +const buildFeastLink = (recipeId: string, stage: StageType): string => { + const token = + stage === 'PROD' ? FEAST_ADJUST_TOKEN_PROD : FEAST_ADJUST_TOKEN_CODE; + return `https://guardian-feast.go.link/recipe/${encodeURIComponent( + recipeId, + )}?adj_t=${encodeURIComponent(token)}`; +}; + +// ── Button themes ───────────────────────────────────────────────────────────── + +const primaryCtaTheme = { + backgroundPrimary: FEAST_GREEN, + backgroundPrimaryHover: FEAST_GREEN_HOVER, + textPrimary: sourcePalette.neutral[100], +} as const; + +// ── Grid template ───────────────────────────────────────────────────────────── + +const cardGridStyles = css` + grid-template: + 'image info' + 'buttons buttons' + 'details details' / 1fr 1fr; + ${from.mobileLandscape} { + grid-template: + 'image info' auto + 'image buttons' 1fr + 'details details' / 1fr 1fr; + } +`; + +// ── Card styles ─────────────────────────────────────────────────────────────── + +const showcaseCardStyles = css` + ${lightVars}; + padding: 5px ${space[2]}px 10px ${space[2]}px; + max-width: 100%; + background-color: var(--feast-nudge-bg); + border-top: 1px solid ${FEAST_GREEN}; + display: flex; + flex-direction: column; + gap: ${space[2]}px; + margin: ${space[2]}px 0; +`; + +// ── Grid area styles ────────────────────────────────────────────────────────── + +const productInfoContainerStyles = css` + display: flex; + flex-direction: column; + gap: ${space[1]}px; +`; + +const buttonWrapperStyles = css` + grid-area: buttons; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: ${space[1]}px; +`; + +const descriptionStyles = css` + ${article15}; + color: var(--feast-nudge-subtext); + b { + font-weight: bold; + } +`; + +// ── Props ───────────────────────────────────────────────────────────────────── + +type FeastContextualNudgeProps = { + recipe: RecipeBlockElement; + recipeArticleTitle: string; + pageId: string; + isDev: boolean; +}; + +/** + * Inline contextual nudge prompting readers to open the current recipe in the + * Feast app. Rendered as an island so that client-only concerns (stage + * detection, dark-mode, Storybook colour-scheme) are handled safely without + * hydration mismatches. + * + * ## Why does this need to be an Island? + * + * The component reads `window.guardian.config.stage` at runtime to build the + * correct Adjust deep-link token. Reading `window.*` during SSR would cause a + * hydration mismatch; wrapping it as an island means it is only ever executed + * on the client. + */ +export const FeastContextualNudge = ({ + recipe, + recipeArticleTitle, + pageId, + isDev, +}: FeastContextualNudgeProps) => { + const abTests = useAB(); + const isVariant = + abTests?.isUserInTestGroup('feast-recipe-nudge-v2', 'variant-1') ?? + false; + + const { darkModeAvailable } = useConfig(); + + const [isStorybook, setIsStorybook] = useState(false); + useEffect(() => { + if (!('STORIES' in window)) return; + setIsStorybook(true); + }, []); + + const [stage, setStage] = useState('PROD'); + useEffect(() => { + setStage(window.guardian.config.stage); + }, []); + + const title = recipe.title ?? recipeArticleTitle; + const feastId = recipe.id; + + useEffect(() => { + if (isDev && isVariant) { + console.log( + `Contextual nudge for the Feast app, related to the recipe: ${title}. (id: ${feastId}; pageId: ${pageId})`, + ); + } + }, [feastId, title, pageId, isDev, isVariant]); + + if (!isVariant) return null; + + return ( +
    + {/* info: title · id */} +
    +
    + Want to save this recipe? Download the Guardian Feast + app to add this to your collection. +
    +
    + + {/* buttons */} +
    + + Download the app + +
    +
    + ); +}; diff --git a/dotcom-rendering/src/components/FeastContextualNudge.stories.tsx b/dotcom-rendering/src/components/FeastContextualNudge.stories.tsx new file mode 100644 index 00000000000..3f5019fc7c4 --- /dev/null +++ b/dotcom-rendering/src/components/FeastContextualNudge.stories.tsx @@ -0,0 +1,85 @@ +import type { Meta, StoryObj } from '@storybook/react-webpack5'; +import { mocked } from 'storybook/test'; +import { darkDecorator } from '../../.storybook/decorators/themeDecorator'; +import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; +import { useAB } from '../lib/useAB'; +import type { RecipeBlockElement } from '../types/content'; +import { FeastContextualNudge } from './FeastContextualNudge.island'; + +const mockBetaABVariant1 = () => { + mocked(useAB).mockReturnValue({ + isUserInTestGroup: (_testId: string, group: string) => + group === 'variant-1', + isUserInTest: () => true, + getParticipations: () => ({}), + trackABTests: () => ({}), + }); +}; + +const mockBetaABControl = () => { + mocked(useAB).mockReturnValue({ + isUserInTestGroup: (_testId: string, group: string) => + group === 'control', + isUserInTest: () => true, + getParticipations: () => ({}), + trackABTests: () => ({}), + }); +}; + +const recipeFormat = { + design: ArticleDesign.Recipe, + display: ArticleDisplay.Standard, + theme: Pillar.Lifestyle, +}; + +const mockRecipe: RecipeBlockElement = { + _type: 'model.dotcomrendering.pageElements.RecipeBlockElement', + id: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4', + title: "Meera Sodha's spring onion pancakes", + description: + 'Crisp and savoury, these spring onion pancakes are a perfect weekend brunch or a quick weeknight snack. Serve with the chilli oil for a serious kick.', + featuredImage: { + url: 'https://i.guim.co.uk/img/media/9dcb491db0315d5598fc47aa51d049e12eedcbbc/0_18_2831_3539/master/2831.jpg?width=620&dpr=2&s=none&crop=none', + mediaId: 'a4a7c9d0d6a5b2e0f0c0d1e2f3a4b5c6d7e8f9a0', + cropId: '0_0_5000_3000', + caption: 'Spring onion pancakes with chilli oil', + }, +}; +const meta = { + title: 'Components/FeastContextualNudge', + component: FeastContextualNudge, + args: { + pageId: 'food/2021/feb/06/meera-sodhas-vegan-recipe-for-spring-onion-pancakes', + recipeArticleTitle: "Meera Sodha's spring onion pancakes", + recipe: mockRecipe, + isDev: true, + }, + parameters: { + chromatic: { viewports: [375, 740, 980] }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +/** Default — recipe name + CTAs */ +export const Default: Story = { + beforeEach() { + mockBetaABVariant1(); + }, +}; + +/** Dark mode */ +export const DefaultDark: Story = { + beforeEach() { + mockBetaABVariant1(); + }, + decorators: [darkDecorator([recipeFormat])], +}; + +/** Control — nudge is not shown */ +export const Control: Story = { + beforeEach() { + mockBetaABControl(); + }, +}; diff --git a/dotcom-rendering/src/components/FeatureCard.tsx b/dotcom-rendering/src/components/FeatureCard.tsx index aec4d72f1e2..f2c069c24fa 100644 --- a/dotcom-rendering/src/components/FeatureCard.tsx +++ b/dotcom-rendering/src/components/FeatureCard.tsx @@ -83,7 +83,7 @@ const underlineOnHoverStyles = css` } `; -const hoverStyles = css` +const hoverStyles = (isLoopAndInLoopClickTestVariant: boolean) => css` :hover .media-overlay { position: absolute; top: 0; @@ -91,6 +91,8 @@ const hoverStyles = css` width: 100%; height: 100%; background-color: ${palette('--card-background-hover')}; + + ${isLoopAndInLoopClickTestVariant && loopClickThroughOverlayStyles} } ${underlineOnHoverStyles} @@ -106,6 +108,12 @@ const hoverStyles = css` } `; +const loopClickThroughOverlayStyles = css` + z-index: ${getZIndex('mediaOverlay')}; + cursor: pointer; + pointer-events: none; +`; + const contentStyles = css` display: flex; flex-basis: 100%; @@ -507,9 +515,15 @@ export const FeatureCard = ({ const aspectRatioNumber = isImmersive ? 5 / 3 : 4 / 5; - /* The whole card is clickable on cinemagraphs and pictures */ + /** The whole card is clickable for: + * - cinemagraphs + * - pictures + * - loops in the loop click test variant group + * */ const allowLinkThroughOverlay = - media.style === 'cinemagraph' || media.type === 'picture'; + media.style === 'cinemagraph' || + media.type === 'picture' || + isLoopAndInLoopClickTestVariant; return ( @@ -517,7 +531,9 @@ export const FeatureCard = ({
    {!isYoutubeVideo && !isSelfHostedVideoWithControls && ( @@ -692,7 +708,8 @@ export const FeatureCard = ({ )} {/* This overlay is styled when the CardLink is hovered */} - {!isSelfHostedVideoWithControls && ( + {(!isSelfHostedVideoWithControls || + isLoopAndInLoopClickTestVariant) && (
    )}
    { +export const FetchHostedOnwards = ({ + branding, + url, + isGalleryPage = false, +}: Props) => { const { data, error } = useApi(url); if (error) { @@ -31,6 +36,7 @@ export const FetchHostedOnwards = ({ branding, url }: Props) => { ); }; diff --git a/dotcom-rendering/src/components/FollowButtons.stories.tsx b/dotcom-rendering/src/components/FollowTagButton.stories.tsx similarity index 53% rename from dotcom-rendering/src/components/FollowButtons.stories.tsx rename to dotcom-rendering/src/components/FollowTagButton.stories.tsx index f6b4a0cb2bb..2451fe3b700 100644 --- a/dotcom-rendering/src/components/FollowButtons.stories.tsx +++ b/dotcom-rendering/src/components/FollowTagButton.stories.tsx @@ -1,10 +1,10 @@ import { splitTheme } from '../../.storybook/decorators/splitThemeDecorator'; import { ArticleDesign, ArticleDisplay, Pillar } from '../lib/articleFormat'; -import { FollowNotificationsButton, FollowTagButton } from './FollowButtons'; +import { FollowTagButton } from './FollowTagButton'; export default { - component: [FollowNotificationsButton, FollowTagButton], - title: 'Components/FollowStatus', + component: FollowTagButton, + title: 'Components/FollowTagButton', args: { isFollowing: false, }, @@ -12,17 +12,11 @@ export default { export const Default = ({ isFollowing }: { isFollowing: boolean }) => { return ( - <> - undefined} - /> - undefined} - /> - + undefined} + /> ); }; @@ -37,22 +31,6 @@ Default.decorators = [ ]), ]; -export const NotificationsButtonBothStates = () => { - return ( - <> - undefined} - /> - undefined} - /> - - ); -}; -NotificationsButtonBothStates.decorators = [splitTheme()]; - export const FollowContributorBothStates = () => { return ( <> diff --git a/dotcom-rendering/src/components/FollowButtons.test.tsx b/dotcom-rendering/src/components/FollowTagButton.test.tsx similarity index 62% rename from dotcom-rendering/src/components/FollowButtons.test.tsx rename to dotcom-rendering/src/components/FollowTagButton.test.tsx index 102f60f8bae..02f1dc7bae4 100644 --- a/dotcom-rendering/src/components/FollowButtons.test.tsx +++ b/dotcom-rendering/src/components/FollowTagButton.test.tsx @@ -1,27 +1,5 @@ import { render, waitFor } from '@testing-library/react'; -import { FollowNotificationsButton, FollowTagButton } from './FollowButtons'; - -it('should show a Notifications Off button for a single contributor when rendering for apps', () => { - const { getByText } = render( - undefined} - />, - ); - expect(getByText('Notifications off')).toBeInTheDocument(); -}); - -it('should show a Notifications On button for a single contributor when rendering for apps', async () => { - const { getByText } = render( - undefined} - isFollowing={true} - />, - ); - await waitFor(() => - expect(getByText('Notifications on')).toBeInTheDocument(), - ); -}); +import { FollowTagButton } from './FollowTagButton'; it('should show a follow contributor button for a single contributor when rendering for apps', () => { const { getByText } = render( diff --git a/dotcom-rendering/src/components/FollowButtons.tsx b/dotcom-rendering/src/components/FollowTagButton.tsx similarity index 72% rename from dotcom-rendering/src/components/FollowButtons.tsx rename to dotcom-rendering/src/components/FollowTagButton.tsx index d2dce658e10..d454fad3a31 100644 --- a/dotcom-rendering/src/components/FollowButtons.tsx +++ b/dotcom-rendering/src/components/FollowTagButton.tsx @@ -1,11 +1,6 @@ import { css } from '@emotion/react'; import { space, textSans15 } from '@guardian/source/foundations'; -import { - SvgCheckmark, - SvgNotificationsOff, - SvgNotificationsOn, - SvgPlus, -} from '@guardian/source/react-components'; +import { SvgCheckmark, SvgPlus } from '@guardian/source/react-components'; import type { ReactNode } from 'react'; import { palette } from '../palette'; @@ -65,12 +60,6 @@ const containerStyles = css` column-gap: 0.2em; `; -const notificationsTextSpan = ({ - isFollowing, -}: Pick) => ( - Notifications {isFollowing ? 'on' : 'off'} -); - const tagTextSpan = ({ isFollowing, displayName, @@ -90,29 +79,6 @@ type ButtonProps = { onClickHandler: () => void; }; -export const FollowNotificationsButton = ({ - isFollowing, - onClickHandler, - withExtraBottomMargin = false, -}: ButtonProps & { withExtraBottomMargin?: boolean }) => { - return ( - - ); -}; - export const FollowTagButton = ({ isFollowing, displayName = '', diff --git a/dotcom-rendering/src/components/FollowWrapper.island.tsx b/dotcom-rendering/src/components/FollowWrapper.island.tsx index 7208609214c..c2ed8d2ef9d 100644 --- a/dotcom-rendering/src/components/FollowWrapper.island.tsx +++ b/dotcom-rendering/src/components/FollowWrapper.island.tsx @@ -1,18 +1,34 @@ import { css } from '@emotion/react'; import { Topic } from '@guardian/bridget/Topic'; import { isUndefined, log } from '@guardian/libs'; -import { from, space } from '@guardian/source/foundations'; +import { from, space, textSans12 } from '@guardian/source/foundations'; +import { + SvgNotificationsOff, + SvgNotificationsOn, +} from '@guardian/source/react-components'; import { useEffect, useState } from 'react'; import { getNotificationsClient, getTagClient } from '../lib/bridgetApi'; import { useIsBridgetCompatible } from '../lib/useIsBridgetCompatible'; import { useIsMyGuardianEnabled } from '../lib/useIsMyGuardianEnabled'; -import { FollowNotificationsButton, FollowTagButton } from './FollowButtons'; +import { palette as schemedPalette } from '../palette'; +import { FollowTagButton } from './FollowTagButton'; type Props = { id: string; displayName: string; }; +const notificationTextStyles = css` + ${textSans12} + color: ${schemedPalette('--follow-text')}; + fill: currentColor; + display: flex; + align-items: center; + column-gap: ${space[1]}px; + min-height: ${space[6]}px; + padding: 0; +`; + export const FollowWrapper = ({ id, displayName }: Props) => { const [isFollowingNotifications, setIsFollowingNotifications] = useState< boolean | undefined @@ -20,9 +36,6 @@ export const FollowWrapper = ({ id, displayName }: Props) => { const [isFollowingTag, setIsFollowingTag] = useState( undefined, ); - const [showFollowTagButton, setShowFollowTagButton] = - useState(false); - const isMyGuardianEnabled = useIsMyGuardianEnabled(); const isBridgetCompatible = useIsBridgetCompatible('2.5.0'); @@ -30,10 +43,8 @@ export const FollowWrapper = ({ id, displayName }: Props) => { const blockList = ['profile/anas-al-sharif']; return !blockList.includes(tagId); }; - - if (isBridgetCompatible && isMyGuardianEnabled && isNotInBlockList(id)) { - setShowFollowTagButton(true); - } + const showFollowTagButton = + isBridgetCompatible && isMyGuardianEnabled && isNotInBlockList(id); useEffect(() => { const topic = new Topic({ @@ -42,31 +53,62 @@ export const FollowWrapper = ({ id, displayName }: Props) => { type: 'tag-contributor', }); - void getNotificationsClient() - .isFollowing(topic) - .then(setIsFollowingNotifications) - .catch((error) => { - window.guardian.modules.sentry.reportError( - error, - 'bridget-getNotificationsClient-isFollowing-error', - ); - log( - 'dotcom', - 'Bridget getNotificationsClient.isFollowing Error:', - error, - ); - }); + void Promise.all([ + getNotificationsClient() + .isFollowing(topic) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getNotificationsClient-isFollowing-error', + ); + log( + 'dotcom', + 'Bridget getNotificationsClient.isFollowing Error:', + error, + ); + return undefined; + }), + getTagClient() + .isFollowing(topic) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getTagClient-isFollowing-error', + ); + log( + 'dotcom', + 'Bridget getTagClient.isFollowing Error:', + error, + ); + return undefined; + }), + ]).then(([followingNotifications, followingTag]) => { + setIsFollowingNotifications(followingNotifications); + setIsFollowingTag(followingTag); - void getTagClient() - .isFollowing(topic) - .then(setIsFollowingTag) - .catch((error) => { - window.guardian.modules.sentry.reportError( - error, - 'bridget-getTagClient-isFollowing-error', - ); - log('dotcom', 'Bridget getTagClient.isFollowing Error:', error); - }); + // Legacy: if user has notifications on but isn't following, + // auto-follow the tag to keep states consistent + if (followingNotifications && !followingTag) { + void getTagClient() + .follow(topic) + .then((success) => { + if (success) { + setIsFollowingTag(true); + } + }) + .catch((error) => { + window.guardian.modules.sentry.reportError( + error, + 'bridget-getTagClient-auto-follow-error', + ); + log( + 'dotcom', + 'Bridget getTagClient.follow (auto) Error:', + error, + ); + }); + } + }); }, [id, displayName]); const tagHandler = () => { @@ -95,32 +137,8 @@ export const FollowWrapper = ({ id, displayName }: Props) => { error, ); }); - } else { - void getTagClient() - .follow(topic) - .then((success) => { - if (success) { - setIsFollowingTag(true); - } - }) - .catch((error) => { - window.guardian.modules.sentry.reportError( - error, - 'bridget-getTagClient-follow-error', - ); - log('dotcom', 'Bridget getTagClient.follow Error:', error); - }); - } - }; - const notificationsHandler = () => { - const topic = new Topic({ - id, - displayName, - type: 'tag-contributor', - }); - - if (isFollowingNotifications) { + // Turn off notifications when unfollowing void getNotificationsClient() .unfollow(topic) .then((success) => { @@ -131,7 +149,7 @@ export const FollowWrapper = ({ id, displayName }: Props) => { .catch((error) => { window.guardian.modules.sentry.reportError( error, - 'briidget-getNotificationsClient-unfollow-error', + 'bridget-getNotificationsClient-unfollow-error', ); log( 'dotcom', @@ -140,8 +158,15 @@ export const FollowWrapper = ({ id, displayName }: Props) => { ); }); } else { - void getNotificationsClient() + void getTagClient() .follow(topic) + .then((success) => { + if (success) { + setIsFollowingTag(true); + return getNotificationsClient().follow(topic); + } + return false; + }) .then((success) => { if (success) { setIsFollowingNotifications(true); @@ -150,13 +175,9 @@ export const FollowWrapper = ({ id, displayName }: Props) => { .catch((error) => { window.guardian.modules.sentry.reportError( error, - 'bridget-getNotificationsClient-follow-error', - ); - log( - 'dotcom', - 'Bridget getNotificationsClient.follow Error:', - error, + 'bridget-getTagClient-follow-error', ); + log('dotcom', 'Bridget getTagClient.follow Error:', error); }); } }; @@ -189,14 +210,23 @@ export const FollowWrapper = ({ id, displayName }: Props) => { withExtraBottomMargin={true} /> )} - undefined - } - /> + {isFollowingTag && ( + + {isFollowingNotifications ? ( + <> + + Notifications turned on. Turn off anytime in + Settings. + + ) : ( + <> + + Notifications turned off. Turn on anytime in + Settings. + + )} + + )}
    ); }; diff --git a/dotcom-rendering/src/components/FootballMatchDay.tsx b/dotcom-rendering/src/components/FootballMatchDay.tsx index 7410b30f07d..6c2c3c23c38 100644 --- a/dotcom-rendering/src/components/FootballMatchDay.tsx +++ b/dotcom-rendering/src/components/FootballMatchDay.tsx @@ -90,9 +90,9 @@ const containerCss = css` flex-direction: column; gap: ${space[2]}px; - .ios, - .android { - @media (prefers-color-scheme: dark) { + @media (prefers-color-scheme: dark) { + .ios &, + .android & { --match-day-text: ${neutral[86]}; --match-day-text-live: ${neutral[7]}; --match-day-text-result: ${neutral[97]}; @@ -317,6 +317,10 @@ const teamCss = css` display: flex; align-items: center; gap: ${space[1]}px; + text-align: right; + ${from.phablet} { + padding-left: 8ch; + } `; const awayTeamCss = css` @@ -324,6 +328,10 @@ const awayTeamCss = css` flex-direction: row-reverse; justify-self: start; padding-right: ${space[4]}px; + text-align: left; + ${from.phablet} { + padding-left: 0; + } `; const Score = ({ match }: { match: FootballMatch }) => { diff --git a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.stories.tsx b/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.stories.tsx index 408b57e8c99..2b1c9b0701f 100644 --- a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.stories.tsx +++ b/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.stories.tsx @@ -90,8 +90,9 @@ export const FixtureWeb = meta.story({ paId: 'matchId', }, tabs: { - selected: 'info', - matchKind: 'Fixture', + infoURL: new URL( + 'https://www.theguardian.com/football/match/2025/nov/26/arsenal-v-bayernmunich', + ), }, }, leagueName: 'Premier League', diff --git a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx b/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx index 1d910a241af..2fa873b0eb4 100644 --- a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx +++ b/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeader.tsx @@ -13,7 +13,7 @@ import { textSansItalic15Object, until, } from '@guardian/source/foundations'; -import { type ComponentProps, useMemo } from 'react'; +import { useMemo } from 'react'; import type { SWRConfiguration } from 'swr'; import useSWR from 'swr'; import type { FootballMatch } from '../../footballMatchV2'; @@ -35,16 +35,17 @@ import type { ArticleDeprecated } from '../../types/article'; import type { RenderingTarget } from '../../types/renderingTarget'; import { BigNumber } from '../BigNumber'; import { FootballCrest } from '../FootballCrest'; +import { MatchHeaderFallback } from '../MatchHeaderFallback'; import { Placeholder } from '../Placeholder'; import { background, border, primaryText, secondaryText } from './colours'; -import { FootballMatchHeaderFallback } from './FootballMatchHeaderFallback'; import { type HeaderData, parse as parseHeaderData } from './headerData'; import { Hr } from './Hr'; import { Notifications } from './Notifications'; +import type { TabName } from './Tabs'; import { Tabs } from './Tabs'; export type FootballMatchHeaderProps = { - initialTab: ComponentProps['selected']; + initialTab: TabName; initialData?: HeaderData; leagueName: string; leagueURL?: string; @@ -82,7 +83,7 @@ export const FootballMatchHeader = (props: Props) => { props.format.design === ArticleDesign.DeadBlog) ) { return ( - @@ -113,14 +114,18 @@ export const FootballMatchHeader = (props: Props) => { >
    @@ -142,7 +147,14 @@ export const FootballMatchHeader = (props: Props) => { liveActivitiesClient={props.liveActivitiesClient} />
    - +
    ); @@ -160,7 +172,7 @@ const swrOptions = (refreshInterval: number): SWRConfiguration => ({ const fetcher = ( - selected: Props['initialTab'], + selected: TabName, renderingTarget: RenderingTarget, getHeaderData: Props['getHeaderData'], ) => @@ -330,18 +342,27 @@ const Team = (props: { flex: '1 1 50%', wordBreak: 'break-word', borderLeftStyle: 'solid', + borderLeftColor: 'var(--border-left-colour)', '&:last-of-type': { paddingLeft: space[2], borderLeftWidth: 1, }, [from.leftCol]: { - paddingLeft: space[2], - borderLeftWidth: 1, paddingTop: space[3], + position: 'relative', + '&:first-of-type::before': { + content: '""', + top: 0, + left: -10, + width: 1, + backgroundColor: 'var(--border-left-colour)', + position: 'absolute', + height: '100%', + }, }, }} style={{ - borderLeftColor: palette(border(props.match.kind)), + '--border-left-colour': palette(border(props.match.kind)), }} > diff --git a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeaderFallback.stories.tsx b/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeaderFallback.stories.tsx deleted file mode 100644 index 6f0640372c5..00000000000 --- a/dotcom-rendering/src/components/FootballMatchHeader/FootballMatchHeaderFallback.stories.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import preview from '../../../.storybook/preview'; -import { ArticleDesign, ArticleDisplay, Pillar } from '../../lib/articleFormat'; -import type { ArticleDeprecated } from '../../types/article'; -import { FootballMatchHeaderFallback as FootballMatchHeaderFallbackComponent } from './FootballMatchHeaderFallback'; - -const article = { - headline: - 'Australia 3-3 South Korea: Women’s Asian Cup 2026 – as it happened', - sectionLabel: "Women's Asian Cup 2026", - sectionUrl: 'football/womens-asian-cup-2026', - guardianBaseURL: 'https://www.theguardian.com', - webPublicationDate: '2026-03-08T11:46:24.000Z', - tags: [ - { - id: 'football/womens-asian-cup-2026', - type: 'Keyword', - title: "Women's Asian Cup 2026", - }, - ], -} as ArticleDeprecated; - -const meta = preview.meta({ - title: 'Components/Football Match Header Fallback', - component: FootballMatchHeaderFallbackComponent, - args: { - format: { - theme: Pillar.Sport, - design: ArticleDesign.LiveBlog, - display: ArticleDisplay.Standard, - }, - article, - }, -}); - -export const FootballMatchHeaderFallback = meta.story(); diff --git a/dotcom-rendering/src/components/FootballMatchHeader/Notifications.stories.tsx b/dotcom-rendering/src/components/FootballMatchHeader/Notifications.stories.tsx index 38f95081955..a3a09d585a2 100644 --- a/dotcom-rendering/src/components/FootballMatchHeader/Notifications.stories.tsx +++ b/dotcom-rendering/src/components/FootballMatchHeader/Notifications.stories.tsx @@ -1,6 +1,10 @@ +import { fn } from 'storybook/test'; import { gridContainerDecorator } from '../../../.storybook/decorators/gridDecorators'; import preview from '../../../.storybook/preview'; -import type { MatchNotificationsClient } from '../../lib/bridgetApi'; +import type { + LiveActivitiesClient, + MatchNotificationsClient, +} from '../../lib/bridgetApi'; import { palette } from '../../palette'; import { NotificationsToggle } from '../NotificationsToggle.stories'; import { background } from './colours'; @@ -11,7 +15,34 @@ const meta = preview.meta({ component: Notifications, }); -export const Fixture = meta.story({ +const mockLiveActivitiesClient = ( + initFollowing: boolean, + isAvailable: boolean, +): LiveActivitiesClient => { + let following = initFollowing; + + return { + isAvailable: fn(() => { + return Promise.resolve(isAvailable); + }), + + follow: fn(() => { + following = true; + return Promise.resolve(true); + }), + + unfollow: fn(() => { + following = false; + return Promise.resolve(true); + }), + + isFollowing: fn(() => { + return Promise.resolve(following); + }), + }; +}; + +export const FixtureLiveActivity = meta.story({ args: { match: FixtureWeb.input.args.initialData.match, edition: 'UK', @@ -19,7 +50,7 @@ export const Fixture = meta.story({ matchNotificationsClient: FixtureWeb.input.args.matchNotificationsClient, environmentClient: FixtureWeb.input.args.environmentClient, - liveActivitiesClient: FixtureWeb.input.args.liveActivitiesClient, + liveActivitiesClient: mockLiveActivitiesClient(false, true), }, decorators: [gridContainerDecorator], parameters: { @@ -33,20 +64,26 @@ export const Fixture = meta.story({ }, }); -export const Live = Fixture.extend({ +export const FixtureNotifications = FixtureLiveActivity.extend({ + args: { + liveActivitiesClient: mockLiveActivitiesClient(false, false), + }, +}); + +export const LiveLiveActivity = FixtureLiveActivity.extend({ args: { edition: 'AU', match: { - ...Fixture.input.args.match, + ...FixtureLiveActivity.input.args.match, kind: 'Live', status: 'HT', homeTeam: { - ...Fixture.input.args.match.homeTeam, + ...FixtureLiveActivity.input.args.match.homeTeam, score: 1, scorers: [], }, awayTeam: { - ...Fixture.input.args.match.awayTeam, + ...FixtureLiveActivity.input.args.match.awayTeam, score: 0, scorers: [], }, @@ -61,25 +98,41 @@ export const Live = Fixture.extend({ }, }); +export const LiveNotifications = LiveLiveActivity.extend({ + args: { + liveActivitiesClient: mockLiveActivitiesClient(false, false), + }, +}); + /** - * This should be blank. You can't sign up for notifications once a match is + * This should be blank. You can't sign up for live activities once a match is * over. */ -export const Result = Live.extend({ +export const ResultLiveActivity = LiveLiveActivity.extend({ args: { edition: 'US', match: { - ...Live.input.args.match, + ...LiveLiveActivity.input.args.match, kind: 'Result', }, }, }); /** - * When the user already has team notifications for one of the teams, the app - * returns `isAvailable: false` so we show a message instead of the toggle. + * This should be blank. You can't sign up for notifications once a match is + * over. + */ +export const ResultNotifications = ResultLiveActivity.extend({ + args: { + liveActivitiesClient: mockLiveActivitiesClient(false, false), + }, +}); + +/** + * Even if a user is following a team, we should allow them to control the live + * activity here. */ -export const Unavailable = Fixture.extend({ +export const FollowingTeamLiveActivity = FixtureLiveActivity.extend({ args: { matchNotificationsClient: { isAvailable: () => @@ -91,3 +144,14 @@ export const Unavailable = Fixture.extend({ } satisfies MatchNotificationsClient, }, }); + +/** + * When the user already has team notifications for one of the teams, the app + * returns `isAvailable: false` so we show a message instead of the toggle. + */ +export const FollowingTeamNotifications = FixtureNotifications.extend({ + args: { + matchNotificationsClient: + FollowingTeamLiveActivity.input.args.matchNotificationsClient, + }, +}); diff --git a/dotcom-rendering/src/components/FootballMatchHeader/Notifications.tsx b/dotcom-rendering/src/components/FootballMatchHeader/Notifications.tsx index cebe8691590..d6ba831efde 100644 --- a/dotcom-rendering/src/components/FootballMatchHeader/Notifications.tsx +++ b/dotcom-rendering/src/components/FootballMatchHeader/Notifications.tsx @@ -85,7 +85,12 @@ const useAvailability = ( useEffect(() => { getAvailability( renderingTarget, - match, + match.kind, + match.paId, + match.homeTeam.name, + match.homeTeam.paID, + match.awayTeam.name, + match.awayTeam.paID, environment, liveActivities, matchNotifications, @@ -107,7 +112,12 @@ const useAvailability = ( }); }, [ renderingTarget, - match, + match.kind, + match.paId, + match.homeTeam.name, + match.homeTeam.paID, + match.awayTeam.name, + match.awayTeam.paID, environment, liveActivities, matchNotifications, @@ -118,30 +128,35 @@ const useAvailability = ( const getAvailability = async ( renderingTarget: RenderingTarget, - match: FootballMatch, + matchKind: FootballMatch['kind'], + matchId: FootballMatch['paId'], + homeTeamName: FootballMatch['homeTeam']['name'], + homeTeamId: FootballMatch['homeTeam']['paID'], + awayTeamName: FootballMatch['awayTeam']['name'], + awayTeamId: FootballMatch['awayTeam']['paID'], environment: EnvironmentClient, liveActivities: LiveActivitiesClient, matchNotifications: MatchNotificationsClient, ): Promise => { - if (renderingTarget !== 'Apps' || match.kind === 'Result') { + if (renderingTarget !== 'Apps' || matchKind === 'Result') { return { kind: 'none' }; } if ( (await hasLiveActivitiesSupport(environment)) && - (await liveActivities.isAvailable('football-match', match.paId)) + (await liveActivities.isAvailable('football-match', matchId)) ) { return { kind: 'liveActivities' }; } const notificationsAvailability = await matchNotifications.isAvailable({ homeTeam: { - paId: match.homeTeam.paID, - name: match.homeTeam.name, + paId: homeTeamId, + name: homeTeamName, }, awayTeam: { - paId: match.awayTeam.paID, - name: match.awayTeam.name, + paId: awayTeamId, + name: awayTeamName, }, }); @@ -213,7 +228,7 @@ const LiveActivities = (props: { }) => ( <>
    - +
    - - Be notified about the lineup, kick-off time, goals, half-time - and full time scores - + ); -const LiveActivityDescription = (props: { - matchKind: FootballMatch['kind']; -}) => { +const MatchDescription = (props: { matchKind: FootballMatch['kind'] }) => { switch (props.matchKind) { case 'Fixture': return ( diff --git a/dotcom-rendering/src/components/FootballMatchHeader/Tabs.stories.tsx b/dotcom-rendering/src/components/FootballMatchHeader/Tabs.stories.tsx index 82450937a12..d3b28a6cac0 100644 --- a/dotcom-rendering/src/components/FootballMatchHeader/Tabs.stories.tsx +++ b/dotcom-rendering/src/components/FootballMatchHeader/Tabs.stories.tsx @@ -10,6 +10,7 @@ const meta = preview.meta({ export const MatchInfoWhenFixture = meta.story({ args: { selected: 'info', + sportKind: 'football', matchKind: 'Fixture', }, parameters: { @@ -25,8 +26,9 @@ export const LiveWhenLive = meta.story({ ...MatchInfoWhenFixture.input, args: { selected: 'live', + sportKind: 'football', matchKind: 'Live', - infoURL: new URL( + infoTab: new URL( 'https://www.theguardian.com/football/match/2025/nov/26/arsenal-v-bayernmunich', ), }, @@ -42,10 +44,29 @@ export const ReportWhenResult = meta.story({ ...MatchInfoWhenFixture.input, args: { selected: 'report', + sportKind: 'football', matchKind: 'Result', - liveURL: new URL( + liveTab: new URL( 'https://www.theguardian.com/football/live/2025/nov/26/arsenal-v-bayern-munich-champions-league-live', ), - infoURL: LiveWhenLive.input.args.infoURL, + infoTab: LiveWhenLive.input.args.infoTab, + }, +}); + +export const ButtonInfoTab = meta.story({ + ...MatchInfoWhenFixture.input, + args: { + selected: 'live', + sportKind: 'football', + matchKind: 'Live', + infoTab: () => { + // + }, + }, + parameters: { + colourSchemeBackground: { + light: palette('--football-match-header-live-background'), + dark: palette('--football-match-header-live-background'), + }, }, }); diff --git a/dotcom-rendering/src/components/FootballMatchHeader/Tabs.tsx b/dotcom-rendering/src/components/FootballMatchHeader/Tabs.tsx index 6b74a0e7b6c..b8e1beec526 100644 --- a/dotcom-rendering/src/components/FootballMatchHeader/Tabs.tsx +++ b/dotcom-rendering/src/components/FootballMatchHeader/Tabs.tsx @@ -13,30 +13,19 @@ import type { RenderingTarget } from '../../types/renderingTarget'; import { useConfig } from '../ConfigContext'; import { border, primaryText, selected } from './colours'; +export type TabName = 'info' | 'live' | 'report'; + type Props = { matchKind: FootballMatch['kind']; - // eslint-disable-next-line react/no-unused-prop-types -- false positive, this is passed into the tab components - sportKind?: 'football' | 'cricket'; -} & ( - | { - selected: 'info'; - reportURL?: URL; - liveURL?: URL; - } - | { - selected: 'live'; - reportURL?: URL; - infoURL: URL; - } - | { - selected: 'report'; - liveURL?: URL; - infoURL: URL; - } -); + sportKind: 'football' | 'cricket'; + selected: TabName; + reportTab?: URL; + liveTab?: URL; + infoTab?: URL | (() => void); +}; export const Tabs = (props: Props) => ( -
    ); diff --git a/dotcom-rendering/src/components/FrontPage.tsx b/dotcom-rendering/src/components/FrontPage.tsx index d35002e01a0..c7e9aa095f3 100644 --- a/dotcom-rendering/src/components/FrontPage.tsx +++ b/dotcom-rendering/src/components/FrontPage.tsx @@ -72,7 +72,6 @@ export const FrontPage = ({ front, NAV }: Props) => { commercialMetricsEnabled={ !!front.config.switches.commercialMetrics } - tests={front.config.abTests} /> @@ -92,10 +91,7 @@ export const FrontPage = ({ front, NAV }: Props) => { - {isGoogleOneTapEnabled( - front.config.abTests, - front.config.switches, - ) && ( + {isGoogleOneTapEnabled(front.config.switches) && ( diff --git a/dotcom-rendering/src/components/GalleryCaption.tsx b/dotcom-rendering/src/components/GalleryCaption.tsx index 26a71ba6205..ddd07129a8b 100644 --- a/dotcom-rendering/src/components/GalleryCaption.tsx +++ b/dotcom-rendering/src/components/GalleryCaption.tsx @@ -1,7 +1,15 @@ import { css } from '@emotion/react'; -import { between, from, space, textSans14 } from '@guardian/source/foundations'; +import { + between, + from, + palette as sourcePalette, + space, + textSans14, + textSans15, + textSansBold17, +} from '@guardian/source/foundations'; import { grid } from '../grid'; -import { type ArticleFormat } from '../lib/articleFormat'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { palette } from '../palette'; import { CaptionText } from './CaptionText'; import { Island } from './Island'; @@ -16,6 +24,11 @@ type Props = { webTitle: string; /** Position of the image in the gallery used to build share fragment */ position?: number; + /** + * Total number of images in the article used to display alongside the image position (1/5, 2/5, etc.) + * Used for hosted content galleries only + */ + imagesLength?: number; }; const styles = css` @@ -31,37 +44,48 @@ const styles = css` ${between.desktop.and.leftCol} { ${grid.column.right} - - position: relative; /* allows the ::before to be positioned relative to this */ - - &::before { - content: ''; - position: absolute; - left: -10px; /* 10px to the left of this element */ - top: 0; - bottom: 0; - width: 1px; - background-color: ${palette('--article-border')}; - } } ${from.leftCol} { ${grid.column.left} + } +`; + +const hostedGalleryStyles = css` + ${textSansBold17} + align-self: end; - position: relative; /* allows the ::before to be positioned relative to this */ - - &::after { - content: ''; - position: absolute; - right: -11px; - top: -12px; - bottom: 0; - width: 1px; - background-color: ${palette('--article-border')}; - } + ${from.tablet} { + padding-bottom: ${space[10]}px; + } + + ${between.tablet.and.desktop} { + padding-left: 0; + padding-right: 0; } `; +const positionIndicatorStyles = css` + ${textSans15} + display: block; + padding: ${space[2]}px 0 ${space[1]}px; + + ${from.desktop} { + padding-top: 0; + } +`; + +const creditStyles = css` + display: block; + padding: ${space[2]}px 0 ${space[2]}px; +`; + +const hostedCreditStyles = css` + ${textSans15} + padding-top: ${space[3]}px; + color: ${sourcePalette.neutral[73]}; +`; + export const GalleryCaption = ({ captionHtml, credit, @@ -70,47 +94,62 @@ export const GalleryCaption = ({ pageId, webTitle, position, + imagesLength, }: Props) => { const emptyCaption = captionHtml === undefined || captionHtml.trim() === ''; const hideCredit = displayCredit === false || credit === undefined || credit === ''; + const isHostedGallery = format.design === ArticleDesign.HostedGallery; + const showPositionIndicator = + isHostedGallery && + typeof position === 'number' && + position !== 0 && + typeof imagesLength === 'number' && + imagesLength !== 0; if (emptyCaption && hideCredit) { return null; } return ( -
    +
    + {showPositionIndicator && ( + + {position}/{imagesLength} + + )} + {emptyCaption ? null : } + {hideCredit ? null : ( {credit} )} -
    - - - -
    + + {!isHostedGallery && ( +
    + + + +
    + )}
    ); }; diff --git a/dotcom-rendering/src/components/GalleryImage.tsx b/dotcom-rendering/src/components/GalleryImage.tsx index 5ce317dcd10..d48011784e3 100644 --- a/dotcom-rendering/src/components/GalleryImage.tsx +++ b/dotcom-rendering/src/components/GalleryImage.tsx @@ -1,11 +1,11 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; -import { from, space, until } from '@guardian/source/foundations'; +import { between, from, space, until } from '@guardian/source/foundations'; import { grid } from '../grid'; -import { type ArticleFormat } from '../lib/articleFormat'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { getImage } from '../lib/image'; import { palette } from '../palette'; -import { type ImageBlockElement } from '../types/content'; +import type { ImageBlockElement } from '../types/content'; import { type RenderingTarget } from '../types/renderingTarget'; import { AppsLightboxImage } from './AppsLightboxImage.island'; import { GalleryCaption } from './GalleryCaption'; @@ -19,11 +19,12 @@ type Props = { pageId: string; webTitle: string; renderingTarget: RenderingTarget; + imagesLength?: number; }; const styles = css` ${grid.paddedContainer} - ${grid.verticalRules()} + ${grid.outerRules()} grid-auto-flow: row dense; background-color: ${palette('--article-inner-background')}; @@ -33,10 +34,23 @@ const styles = css` } ${from.desktop} { - &:first-of-type { + &:first-of-type > * { padding-top: ${space[3]}px; } } + + ${between.desktop.and.leftCol} { + ${grid.centreRule(2)} + } + ${from.leftCol} { + ${grid.centreRule(1)} + } +`; + +const hostedGalleryOverrides = css` + ${between.desktop.and.leftCol} { + ${grid.centreRule(2, 'transparent')} + } `; const galleryBodyImageStyles = css` @@ -63,6 +77,7 @@ export const GalleryImage = ({ pageId, webTitle, renderingTarget, + imagesLength, }: Props) => { const asset = getImage(image.media.allImages); @@ -78,7 +93,13 @@ export const GalleryImage = ({ } return ( -
    +
    ); diff --git a/dotcom-rendering/src/components/GoogleOneTap.island.tsx b/dotcom-rendering/src/components/GoogleOneTap.island.tsx index dd8d32dd8df..cd4de4a8c08 100644 --- a/dotcom-rendering/src/components/GoogleOneTap.island.tsx +++ b/dotcom-rendering/src/components/GoogleOneTap.island.tsx @@ -7,7 +7,7 @@ import { useIsSignedIn } from '../lib/useAuthStatus'; import { useConsent } from '../lib/useConsent'; import { useCountryCode } from '../lib/useCountryCode'; import { useOnce } from '../lib/useOnce'; -import type { ServerSideTests, StageType, Switches } from '../types/config'; +import type { StageType, Switches } from '../types/config'; type IdentityProviderConfig = { configURL: string; @@ -24,11 +24,7 @@ type CredentialsProvider = { }) => Promise<{ token: string }>; }; -export const isGoogleOneTapEnabled = ( - tests: ServerSideTests, - switches: Switches, -): boolean => - tests['googleOneTapVariant'] === 'variant' || +export const isGoogleOneTapEnabled = (switches: Switches): boolean => switches['googleOneTapSwitch'] === true; /** diff --git a/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx b/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx index 7e8b346a956..2f16330d425 100644 --- a/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx +++ b/dotcom-rendering/src/components/HostedContentDisclaimer.stories.tsx @@ -1,13 +1,12 @@ +import { hostedPaletteDecorator } from '../../.storybook/decorators/themeDecorator'; +import preview from '../../.storybook/preview'; import { HostedContentDisclaimer } from './HostedContentDisclaimer'; import { Section } from './Section'; -export default { +const meta = preview.meta({ component: HostedContentDisclaimer, title: 'Components/HostedContentDisclaimer', -}; - -export const Default = () => { - return ( + render: () => (
    { >
    - ); -}; + ), + decorators: hostedPaletteDecorator('#d90c1f'), +}); -Default.storyName = 'default'; +export const Default = meta.story({}); diff --git a/dotcom-rendering/src/components/HostedContentHeader.stories.tsx b/dotcom-rendering/src/components/HostedContentHeader.stories.tsx index bb7f49f78d7..696f494da12 100644 --- a/dotcom-rendering/src/components/HostedContentHeader.stories.tsx +++ b/dotcom-rendering/src/components/HostedContentHeader.stories.tsx @@ -1,31 +1,32 @@ import { palette as sourcePalette } from '@guardian/source/foundations'; +import { hostedPaletteDecorator } from '../../.storybook/decorators/themeDecorator'; import preview from '../../.storybook/preview'; import type { Branding } from '../types/branding'; import { HostedContentHeader } from './HostedContentHeader.island'; import type { Props as HostedContentHeaderProps } from './HostedContentHeader.island'; import { Section } from './Section'; +const branding = { + brandingType: { name: 'paid-content' }, + sponsorName: 'We Are Still In', + logo: { + src: 'https://static.theguardian.com/commercial/sponsor/16/Aug/2018/d5e82ba3-297d-473d-8362-c04f519e5fe1-WASI-logo-grey.png', + dimensions: { + width: 1250, + height: 575, + }, + link: 'https://www.wearestillin.com/', + label: 'Paid for by', + }, + aboutThisLink: + 'https://www.theguardian.com/info/2016/jan/25/content-funding', + hostedCampaignColour: '#d90c1f', +} satisfies Branding; + const meta = preview.meta({ component: HostedContentHeader, title: 'Components/HostedContentHeader', - args: { - branding: { - brandingType: { name: 'paid-content' }, - sponsorName: 'We Are Still In', - logo: { - src: 'https://static.theguardian.com/commercial/sponsor/16/Aug/2018/d5e82ba3-297d-473d-8362-c04f519e5fe1-WASI-logo-grey.png', - dimensions: { - width: 1250, - height: 575, - }, - link: 'https://www.wearestillin.com/', - label: 'Paid for by', - }, - aboutThisLink: - 'https://www.theguardian.com/info/2016/jan/25/content-funding', - hostedCampaignColour: '#d90c1f', - } satisfies Branding, - }, + args: { branding }, render: (args: HostedContentHeaderProps) => (
    ), + decorators: hostedPaletteDecorator(branding.hostedCampaignColour), }); export const Default = meta.story(); diff --git a/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx b/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx index 713ccb47ad5..c0608ad565f 100644 --- a/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx +++ b/dotcom-rendering/src/components/HostedContentOnwards.stories.tsx @@ -1,29 +1,60 @@ +import { css } from '@emotion/react'; +import { hostedPaletteDecorator } from '../../.storybook/decorators/themeDecorator'; +import preview from '../../.storybook/preview'; import { hostedOnwardsTrails } from '../../fixtures/manual/onwardsTrails'; +import type { ArticleFormat } from '../lib/articleFormat'; +import { + ArticleDesign, + ArticleDisplay, + ArticleSpecial, +} from '../lib/articleFormat'; +import { palette } from '../palette'; import { HostedContentOnwards } from './HostedContentOnwards'; -export default { +const hostedArticleFormat: ArticleFormat = { + theme: ArticleSpecial.Labs, + display: ArticleDisplay.Standard, + design: ArticleDesign.HostedArticle, +}; + +const meta = preview.meta({ component: HostedContentOnwards, title: 'Components/HostedContentOnwards', -}; + args: { + trails: hostedOnwardsTrails, + brandName: 'TrendAI', + isGalleryPage: false, + }, + parameters: { + formats: [hostedArticleFormat], + }, + render: (args) => ( +
    + +
    + ), +}); -export const Default = () => { - return ( - - ); -}; +export const Default = meta.story({}); -Default.storyName = 'default'; +export const WithAccentColour = meta.story({ + decorators: hostedPaletteDecorator('#d90c1f'), +}); -export const WithAccentColour = () => { - return ( - - ); +const hostedGalleryFormat: ArticleFormat = { + ...hostedArticleFormat, + design: ArticleDesign.HostedGallery, }; - -WithAccentColour.storyName = 'with accent colour'; +export const HostedGallery = meta.story({ + args: { + isGalleryPage: true, + }, + parameters: { + formats: [hostedGalleryFormat], + }, + decorators: [hostedPaletteDecorator('#d90c1f')], +}); diff --git a/dotcom-rendering/src/components/HostedContentOnwards.tsx b/dotcom-rendering/src/components/HostedContentOnwards.tsx index f1b4904110c..26f9c3181cd 100644 --- a/dotcom-rendering/src/components/HostedContentOnwards.tsx +++ b/dotcom-rendering/src/components/HostedContentOnwards.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import { - palette as sourcePalette, + from, space, textSans17, textSansBold20, @@ -12,26 +12,27 @@ import { HostedContentOnwardsCard } from './HostedContentOnwardsCard'; type HostedContentOnwardsProps = { trails: TrailType[]; brandName: string; + isGalleryPage: boolean; }; +/** + * Override --accent-colour variable at a higher CSS specificity + * for hosted gallery articles only, because this only has a dark design + */ +const galleryOverrides = css` + --accent-colour: ${palette('--onward-text')}; +`; + const headerStyles = css` margin-bottom: ${space[1]}px; - border-top: ${space[2]}px solid - var(--accent-colour, ${sourcePalette.neutral[86]}); + border-top: ${space[2]}px solid var(--accent-colour, var(--onward-text)); `; const headingStyles = css` ${textSans17} padding-top: ${space[2]}px; - color: ${palette('--hosted-content-onwards-heading')}; - - @media (prefers-color-scheme: dark) { - color: ${palette('--hosted-content-onwards-heading')}; - } - - [data-color-scheme='dark'] & { - color: ${palette('--hosted-content-onwards-heading')}; - } + padding-bottom: ${space[2]}px; + color: ${palette('--onward-text')}; span { ${textSansBold20} @@ -47,21 +48,45 @@ const stackedCardsStyles = css` const stackedCardWrapper = css` width: 100%; - border-top: 2px solid ${palette('--onward-content-border')}; - padding-top: ${space[2]}px; - padding-bottom: ${space[2]}px; + border-top: 1px solid ${palette('--article-border')}; + padding: ${space[2]}px 0; &:last-of-type { - padding-bottom: 0; + padding: ${space[2]}px 0 0 0; + } +`; + +const rowCardWrapper = css` + ${stackedCardWrapper} + ${from.desktop} { + width: 100%; + border-top: none; + border-right: 1px solid ${palette('--article-border')}; + padding: 0 ${space[2]}px; + + &:first-of-type { + padding: 0 ${space[2]}px 0 0; + } + &:last-of-type { + border-right: none; + padding: 0 0 0 ${space[2]}px; + } + } +`; + +const galleryStyles = css` + ${from.desktop} { + flex-direction: row; } `; export const HostedContentOnwards = ({ trails, brandName, + isGalleryPage = false, }: HostedContentOnwardsProps) => { return ( - <> +

    More from @@ -69,15 +94,25 @@ export const HostedContentOnwards = ({

    -
      +
        {trails.map((trail) => { return ( -
      • - +
      • +
      • ); })}
      - +
    ); }; diff --git a/dotcom-rendering/src/components/HostedContentOnwardsCard.tsx b/dotcom-rendering/src/components/HostedContentOnwardsCard.tsx index 4aac5b9d864..7902fd66164 100644 --- a/dotcom-rendering/src/components/HostedContentOnwardsCard.tsx +++ b/dotcom-rendering/src/components/HostedContentOnwardsCard.tsx @@ -1,22 +1,15 @@ import { css } from '@emotion/react'; import { space, textSansBold15 } from '@guardian/source/foundations'; import { getZIndex } from '../lib/getZIndex'; +import { generateImageURL } from '../lib/image'; import { palette } from '../palette'; import type { TrailType } from '../types/trails'; type Props = { trail: TrailType; + isGalleryPage?: boolean; }; -type CardPictureProps = { - image: string; - alt: string; -}; - -const imageStyles = css` - width: 120px; -`; - const mediaOverlayContainerStyles = css` position: absolute; top: 0; @@ -61,28 +54,30 @@ const headingStyles = css` color: ${palette('--card-headline')}; `; -const CardPicture = ({ image, alt }: CardPictureProps) => { - return ( - <> - - {alt} - -
    -
    -
    - - ); -}; - -export const HostedContentOnwardsCard = ({ trail }: Props) => { +export const HostedContentOnwardsCard = ({ + trail, + isGalleryPage = false, +}: Props) => { return (

    {trail.headline}

    {!!trail.image && ( - + <> + + {trail.image.altText} + +
    +
    +
    + )}
    ); diff --git a/dotcom-rendering/src/components/HostedContentPage.tsx b/dotcom-rendering/src/components/HostedContentPage.tsx index 4a586f294fe..f95ae6b3d3e 100644 --- a/dotcom-rendering/src/components/HostedContentPage.tsx +++ b/dotcom-rendering/src/components/HostedContentPage.tsx @@ -28,59 +28,65 @@ interface AppProps extends BaseProps { renderingTarget: 'Apps'; } +const decideLayout = (article: Article, renderingTarget: 'Web' | 'Apps') => { + const format = { + design: article.design, + display: article.display, + theme: article.theme, + }; + switch (article.design) { + case ArticleDesign.HostedGallery: + return ( + + ); + case ArticleDesign.HostedVideo: + return ( + + ); + case ArticleDesign.HostedArticle: + return ( + + ); + default: + return null; + } +}; + /** * @description * HostedContentPage is a high level wrapper for hosted content pages on Dotcom. Sets strict mode and some globals */ export const HostedContentPage = (props: WebProps | AppProps) => { - const { - article: { design, display, theme, frontendData }, - renderingTarget, - } = props; - + const { article, renderingTarget } = props; + const { frontendData, design, display, theme } = article; const isWeb = renderingTarget === 'Web'; const { darkModeAvailable } = useConfig(); + const format = { design, display, theme }; - const format = { - design, - display, - theme, - }; - - const decideLayout = () => { - switch (format.design) { - case ArticleDesign.HostedVideo: - return ( - - ); - case ArticleDesign.HostedGallery: - return ( - - ); - case ArticleDesign.HostedArticle: - return ( - - ); - default: - return null; - } - }; + const { branding } = + frontendData.commercialProperties[frontendData.editionId]; return ( - + {isWeb && } { commercialMetricsEnabled={ !!frontendData.config.switches.commercialMetrics } - tests={frontendData.config.abTests} /> @@ -137,7 +142,7 @@ export const HostedContentPage = (props: WebProps | AppProps) => { )} - {decideLayout()} + {decideLayout(article, renderingTarget)} ); }; diff --git a/dotcom-rendering/src/components/Island.test.tsx b/dotcom-rendering/src/components/Island.test.tsx index 7fff3892cdd..31af29fe2c4 100644 --- a/dotcom-rendering/src/components/Island.test.tsx +++ b/dotcom-rendering/src/components/Island.test.tsx @@ -296,7 +296,7 @@ describe('Island: server-side rendering', () => { expect(() => renderToString( - + , ), ).not.toThrow(); diff --git a/dotcom-rendering/src/components/KeyTakeaway.tsx b/dotcom-rendering/src/components/KeyTakeaway.tsx index 51e376c7091..a538296315b 100644 --- a/dotcom-rendering/src/components/KeyTakeaway.tsx +++ b/dotcom-rendering/src/components/KeyTakeaway.tsx @@ -4,7 +4,7 @@ import type { EditionId } from '../lib/edition'; import type { ArticleElementRenderer } from '../lib/renderElement'; import { slugify } from '../model/enhance-H2s'; import { palette } from '../palette'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { KeyTakeaway as KeyTakeawayModel, StarRating, @@ -33,7 +33,6 @@ interface KeyTakeawayProps { pageId: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; editionId: EditionId; hideCaption?: boolean; @@ -53,7 +52,6 @@ export const KeyTakeaway = ({ isAdFreeUser, isSensitive, switches, - abTests, editionId, titleIndex, hideCaption, @@ -87,7 +85,6 @@ export const KeyTakeaway = ({ isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} switches={switches} - abTests={abTests} editionId={editionId} hideCaption={hideCaption} starRating={starRating} diff --git a/dotcom-rendering/src/components/KeyTakeaways.stories.tsx b/dotcom-rendering/src/components/KeyTakeaways.stories.tsx index ba4a77bf897..637edd0b324 100644 --- a/dotcom-rendering/src/components/KeyTakeaways.stories.tsx +++ b/dotcom-rendering/src/components/KeyTakeaways.stories.tsx @@ -49,7 +49,6 @@ export const ThemeVariations = meta.story({ display: ArticleDisplay.Standard, theme: Pillar.News, }, - abTests: {}, /** * This is used for rich links. An empty string isn't technically valid, * but there are no rich links in this example. diff --git a/dotcom-rendering/src/components/KeyTakeaways.tsx b/dotcom-rendering/src/components/KeyTakeaways.tsx index ef3f9122936..6e25db0fb43 100644 --- a/dotcom-rendering/src/components/KeyTakeaways.tsx +++ b/dotcom-rendering/src/components/KeyTakeaways.tsx @@ -3,7 +3,7 @@ import type { ArticleFormat } from '../lib/articleFormat'; import type { EditionId } from '../lib/edition'; import type { ArticleElementRenderer } from '../lib/renderElement'; import { palette } from '../palette'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { KeyTakeaway, StarRating } from '../types/content'; import { KeyTakeaway as KeyTakeawayComponent } from './KeyTakeaway'; @@ -14,7 +14,6 @@ interface KeyTakeawaysProps { pageId: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; editionId: EditionId; hideCaption?: boolean; @@ -44,7 +43,6 @@ export const KeyTakeaways = ({ isAdFreeUser, isSensitive, switches, - abTests, editionId, hideCaption, starRating, @@ -64,7 +62,6 @@ export const KeyTakeaways = ({ isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} switches={switches} - abTests={abTests} editionId={editionId} titleIndex={index + 1} hideCaption={hideCaption} diff --git a/dotcom-rendering/src/components/Lightbox.stories.tsx b/dotcom-rendering/src/components/Lightbox.stories.tsx index fef4d92e983..61c23fea7b0 100644 --- a/dotcom-rendering/src/components/Lightbox.stories.tsx +++ b/dotcom-rendering/src/components/Lightbox.stories.tsx @@ -1,3 +1,4 @@ +import { css } from '@emotion/react'; import { storage } from '@guardian/libs'; import { breakpoints } from '@guardian/source/foundations'; import type { Meta, StoryObj } from '@storybook/react-webpack5'; @@ -265,3 +266,29 @@ export const WithLabs = { ], }, } satisfies Story; + +export const WithHostedAccentColour = { + args: { + format: { + display: ArticleDisplay.Standard, + design: ArticleDesign.HostedGallery, + theme: ArticleSpecial.Labs, + }, + images: [ + { + ...testImage, + title: 'Title', + displayCredit: true, + }, + ], + }, + decorators: (Story) => ( +
    + {Story()} +
    + ), +} satisfies Story; diff --git a/dotcom-rendering/src/components/LiveBlock.stories.tsx b/dotcom-rendering/src/components/LiveBlock.stories.tsx index 0cde52f5c3b..29272045e0b 100644 --- a/dotcom-rendering/src/components/LiveBlock.stories.tsx +++ b/dotcom-rendering/src/components/LiveBlock.stories.tsx @@ -76,7 +76,6 @@ export const VideoAsSecond = () => { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={false} editionId={'UK'} @@ -125,7 +124,6 @@ export const Title = () => { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={false} editionId={'UK'} @@ -195,7 +193,6 @@ export const Video = () => { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={false} editionId={'UK'} @@ -240,7 +237,6 @@ export const RichLink = () => { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={false} editionId={'UK'} @@ -276,7 +272,6 @@ export const FirstImage = () => { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={false} editionId={'UK'} @@ -336,7 +331,6 @@ export const ImageRoles = () => { pageId="" webTitle="" ajaxUrl="" - abTests={{}} switches={{}} isPinnedPost={false} isAdFreeUser={false} @@ -387,7 +381,6 @@ export const Thumbnail = () => { pageId="" webTitle="" ajaxUrl="" - abTests={{}} switches={{}} isPinnedPost={false} isAdFreeUser={false} @@ -426,7 +419,6 @@ export const ImageAndTitle = () => { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={false} editionId={'UK'} @@ -459,7 +451,6 @@ export const Updated = () => { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={false} editionId={'UK'} @@ -494,7 +485,6 @@ export const Contributor = () => { pageId="" webTitle="" ajaxUrl="" - abTests={{}} switches={{}} isPinnedPost={false} isAdFreeUser={false} @@ -529,7 +519,6 @@ export const NoAvatar = () => { pageId="" webTitle="" ajaxUrl="" - abTests={{}} switches={{}} isPinnedPost={false} isAdFreeUser={false} @@ -567,7 +556,6 @@ export const TitleAndContributor = () => { pageId="" webTitle="" ajaxUrl="" - abTests={{}} switches={{}} isPinnedPost={false} isAdFreeUser={false} diff --git a/dotcom-rendering/src/components/LiveBlock.tsx b/dotcom-rendering/src/components/LiveBlock.tsx index 49fbacdf6e5..e616a1c77b1 100644 --- a/dotcom-rendering/src/components/LiveBlock.tsx +++ b/dotcom-rendering/src/components/LiveBlock.tsx @@ -4,7 +4,7 @@ import type { ArticleFormat } from '../lib/articleFormat'; import type { EditionId } from '../lib/edition'; import { RenderArticleElement } from '../lib/renderElement'; import type { Block } from '../types/blocks'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import { Island } from './Island'; import { LastUpdated } from './LastUpdated'; import { LiveBlockContainer } from './LiveBlockContainer'; @@ -19,7 +19,6 @@ type Props = { ajaxUrl: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; isLiveUpdate?: boolean; isPinnedPost: boolean; @@ -39,7 +38,6 @@ export const LiveBlock = ({ ajaxUrl, isAdFreeUser, isSensitive, - abTests, switches, isLiveUpdate, isPinnedPost, @@ -89,7 +87,6 @@ export const LiveBlock = ({ webTitle={webTitle} isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} - abTests={abTests} switches={switches} isPinnedPost={isPinnedPost} editionId={editionId} diff --git a/dotcom-rendering/src/components/LiveBlockContainer.tsx b/dotcom-rendering/src/components/LiveBlockContainer.tsx index 4a47fb3de6e..ed969fafba3 100644 --- a/dotcom-rendering/src/components/LiveBlockContainer.tsx +++ b/dotcom-rendering/src/components/LiveBlockContainer.tsx @@ -110,7 +110,7 @@ const BlockByline = ({ ); }; -const liveBlockContainerStyles = () => css` +const liveBlockContainerStyles = css` padding: ${space[2]}px ${SIDE_MARGIN_MOBILE}px; box-sizing: border-box; margin-bottom: ${space[3]}px; diff --git a/dotcom-rendering/src/components/LiveBlogBlocksAndAdverts.tsx b/dotcom-rendering/src/components/LiveBlogBlocksAndAdverts.tsx index c307637f84c..c6554da96ff 100644 --- a/dotcom-rendering/src/components/LiveBlogBlocksAndAdverts.tsx +++ b/dotcom-rendering/src/components/LiveBlogBlocksAndAdverts.tsx @@ -3,7 +3,7 @@ import type { ArticleFormat } from '../lib/articleFormat'; import type { EditionId } from '../lib/edition'; import { getLiveblogAdPositions } from '../lib/getLiveblogAdPositions'; import type { Block } from '../types/blocks'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import { AdPlaceholder } from './AdPlaceholder.apps'; import { AdSlot } from './AdSlot.web'; import { useConfig } from './ConfigContext'; @@ -18,7 +18,6 @@ type Props = { pageId: string; webTitle: string; ajaxUrl: string; - abTests: ServerSideTests; switches: Switches; isAdFreeUser: boolean; isSensitive: boolean; @@ -43,7 +42,6 @@ export const LiveBlogBlocksAndAdverts = ({ pageId, webTitle, ajaxUrl, - abTests, switches, isAdFreeUser, isSensitive, @@ -67,7 +65,6 @@ export const LiveBlogBlocksAndAdverts = ({ host={host} ajaxUrl={ajaxUrl} isLiveUpdate={isLiveUpdate} - abTests={abTests} switches={switches} isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} diff --git a/dotcom-rendering/src/components/LiveBlogRenderer.tsx b/dotcom-rendering/src/components/LiveBlogRenderer.tsx index b24d1de93e2..af64f79b1fd 100644 --- a/dotcom-rendering/src/components/LiveBlogRenderer.tsx +++ b/dotcom-rendering/src/components/LiveBlogRenderer.tsx @@ -2,7 +2,7 @@ import { Hide } from '@guardian/source/react-components'; import type { ArticleFormat } from '../lib/articleFormat'; import type { EditionId } from '../lib/edition'; import type { Block } from '../types/blocks'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { TagType } from '../types/tag'; import { useConfig } from './ConfigContext'; import { EnhancePinnedPost } from './EnhancePinnedPost.island'; @@ -24,7 +24,6 @@ type Props = { ajaxUrl: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; isLiveUpdate: boolean; sectionId: string; @@ -48,7 +47,6 @@ export const LiveBlogRenderer = ({ pageId, webTitle, ajaxUrl, - abTests, switches, isAdFreeUser, isSensitive, @@ -85,7 +83,6 @@ export const LiveBlogRenderer = ({ host={host} ajaxUrl={ajaxUrl} isLiveUpdate={isLiveUpdate} - abTests={abTests} switches={switches} isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} @@ -122,7 +119,6 @@ export const LiveBlogRenderer = ({ host={host} ajaxUrl={ajaxUrl} isLiveUpdate={isLiveUpdate} - abTests={abTests} switches={switches} isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} diff --git a/dotcom-rendering/src/components/MainMedia.tsx b/dotcom-rendering/src/components/MainMedia.tsx index e912f480019..871c4606715 100644 --- a/dotcom-rendering/src/components/MainMedia.tsx +++ b/dotcom-rendering/src/components/MainMedia.tsx @@ -8,7 +8,7 @@ import { import type { EditionId } from '../lib/edition'; import { getZIndex } from '../lib/getZIndex'; import { RenderArticleElement } from '../lib/renderElement'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { FEElement } from '../types/content'; const mainMedia = css` @@ -90,7 +90,6 @@ type Props = { ajaxUrl: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; editionId: EditionId; shouldHideAds: boolean; @@ -108,7 +107,6 @@ export const MainMedia = ({ ajaxUrl, isAdFreeUser, isSensitive, - abTests, switches, editionId, shouldHideAds, @@ -130,7 +128,6 @@ export const MainMedia = ({ webTitle={webTitle} isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} - abTests={abTests} switches={switches} hideCaption={hideCaption} editionId={editionId} diff --git a/dotcom-rendering/src/components/MainMediaGallery.tsx b/dotcom-rendering/src/components/MainMediaGallery.tsx index e961021076d..3b403ddd565 100644 --- a/dotcom-rendering/src/components/MainMediaGallery.tsx +++ b/dotcom-rendering/src/components/MainMediaGallery.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/react'; import { isUndefined } from '@guardian/libs'; import { from } from '@guardian/source/foundations'; import { grid } from '../grid'; -import { type ArticleFormat } from '../lib/articleFormat'; +import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { getImage } from '../lib/image'; import { type ImageBlockElement } from '../types/content'; import { type RenderingTarget } from '../types/renderingTarget'; @@ -27,11 +27,24 @@ const styles = css` } `; +/** + * We don't show main media on hosted gallery pages + * but need to do this to keep the rest of the grid layout happy + */ +const hostedStyles = css` + ${grid.column.all} + grid-row: 1/8; + height: 0; +`; + export const MainMediaGallery = ({ mainMedia, format, renderingTarget, }: Props) => { + if (format.design === ArticleDesign.HostedGallery) { + return
    ; + } // This is to support some galleries created in 2007 where mainMedia is missing if (isUndefined(mainMedia)) { return
    ; diff --git a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.stories.tsx b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.stories.tsx index 0b181f85bc1..c2e10a60351 100644 --- a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.stories.tsx +++ b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.stories.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import { from } from '@guardian/source/foundations'; import type { Meta, StoryObj } from '@storybook/react-webpack5'; -import { newsletterCard } from '../../../../fixtures/manual/highlights-trails'; +import { newsletterSignupCard } from '../../../../fixtures/manual/highlights-trails'; import { ArticleDesign, ArticleDisplay, @@ -40,11 +40,11 @@ const meta = { design: ArticleDesign.Standard, theme: Pillar.News, }, - newsletter: newsletterCard.newsletterData!, - headlineText: newsletterCard.headline, - linkTo: newsletterCard.url, + newsletter: newsletterSignupCard.newsletterData!, + headlineText: newsletterSignupCard.headline, + linkTo: newsletterSignupCard.url, dataLinkName: 'highlights-newsletter-card | open-signup', - image: newsletterCard.image, + image: newsletterSignupCard.image, imageLoading: 'eager', renderingTarget: 'Web', }, diff --git a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.test.tsx b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.test.tsx index b9dc7c19749..e4e364cf85c 100644 --- a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.test.tsx +++ b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.test.tsx @@ -1,6 +1,6 @@ import '@testing-library/jest-dom'; import { fireEvent, render, screen } from '@testing-library/react'; -import { newsletterCard } from '../../../../fixtures/manual/highlights-trails'; +import { newsletterSignupCard } from '../../../../fixtures/manual/highlights-trails'; import { sendNewsletterSignupEvent } from '../../../lib/newsletterSignupTracking'; import { useIsInView } from '../../../lib/useIsInView'; import { ConfigProvider } from '../../ConfigContext'; @@ -11,6 +11,7 @@ jest.mock('../../../lib/newsletterSignupTracking', () => ({ sendNewsletterSignupEvent: jest.fn(), NEWSLETTER_SIGNUP_COMPONENT_ID: { highlightsCard: (id: string) => `highlights-card-${id}`, + highlightsModal: (id: string) => `highlights-modal-${id}`, }, })); @@ -37,12 +38,12 @@ jest.mock('./HighlightsNewsletterSignupModal', () => ({ })); const defaultProps: React.ComponentProps = { - format: newsletterCard.format, - newsletter: newsletterCard.newsletterData!, - headlineText: newsletterCard.headline, - linkTo: newsletterCard.url, + format: newsletterSignupCard.format, + newsletter: newsletterSignupCard.newsletterData!, + headlineText: newsletterSignupCard.headline, + linkTo: newsletterSignupCard.url, dataLinkName: 'highlights-newsletter-card | open-signup', - image: newsletterCard.image, + image: newsletterSignupCard.image, renderingTarget: 'Web', }; @@ -78,7 +79,7 @@ describe('HighlightsNewsletterCard', () => { setCardInView(false); }); - it('opens signup modal on Web click and tracks click', () => { + it('opens signup modal on Web click and tracks EXPAND', () => { setCardInView(true); renderCard(); @@ -237,6 +238,19 @@ describe('HighlightsNewsletterCard', () => { ); }); + it('sets data-modal-component-id on the link to match the Ophan modal component ID', () => { + renderCard(); + + const link = screen.getByRole('link', { + name: defaultProps.headlineText, + }); + + expect(link).toHaveAttribute( + 'data-modal-component-id', + `highlights-modal-${defaultProps.newsletter.identityName}`, + ); + }); + it('falls back to trail image when newsletter image metadata is absent', () => { renderCard({ newsletter: { diff --git a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.tsx b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.tsx index 4825e3d2e5d..83f07e3c46a 100644 --- a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.tsx +++ b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterCard.tsx @@ -143,6 +143,9 @@ export const HighlightsNewsletterCard = ({ const componentId = NEWSLETTER_SIGNUP_COMPONENT_ID.highlightsCard( newsletter.identityName, ); + const modalComponentId = NEWSLETTER_SIGNUP_COMPONENT_ID.highlightsModal( + newsletter.identityName, + ); const [hasBeenSeen, setIsInViewRef] = useIsInView({}); const hasTrackedView = useRef(false); @@ -163,6 +166,7 @@ export const HighlightsNewsletterCard = ({ const handleClick = (event: React.MouseEvent) => { if (renderingTarget === 'Web') { event.preventDefault(); + setIsModalOpen(true); sendNewsletterSignupEvent({ @@ -191,6 +195,8 @@ export const HighlightsNewsletterCard = ({ {isModalOpen && ( { setIsModalOpen(false); sendNewsletterSignupEvent({ @@ -212,13 +218,14 @@ export const HighlightsNewsletterCard = ({ css={linkOverlayStyles} onClick={handleClick} data-link-name={dataLinkName} + data-modal-component-id={modalComponentId} aria-label={headlineText} />
    - Free newsletter + Newsletter {}, - }, }); export default meta; +type Story = StoryObj; + +const defaultArgs = { + newsletter, + onClose: () => undefined, + renderingTarget: 'Web', + componentId: NEWSLETTER_SIGNUP_COMPONENT_ID.highlightsCard( + newsletter.identityName, + ), +} satisfies Story['args']; + /** Signed-out user who has not yet subscribed – the default state. */ export const SignedOut = meta.story({ + args: { ...defaultArgs }, beforeEach() { mocked(useNewsletterSubscription).mockReturnValue(false); mocked(useIsSignedIn).mockReturnValue(false); @@ -32,6 +42,7 @@ export const SignedOut = meta.story({ /** Signed-in user who has not yet subscribed. */ export const SignedIn = meta.story({ + args: { ...defaultArgs }, beforeEach() { mocked(useNewsletterSubscription).mockReturnValue(false); mocked(useIsSignedIn).mockReturnValue(true); @@ -43,6 +54,7 @@ export const SignedIn = meta.story({ /** User who is already subscribed – the form shows the success/subscribed state. */ export const SignedUp = meta.story({ + args: { ...defaultArgs }, beforeEach() { mocked(useNewsletterSubscription).mockReturnValue(true); mocked(useIsSignedIn).mockReturnValue(true); diff --git a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.test.tsx b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.test.tsx index 22565f08839..485b40fac27 100644 --- a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.test.tsx +++ b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.test.tsx @@ -1,6 +1,7 @@ import '@testing-library/jest-dom'; import { act, fireEvent, render, screen } from '@testing-library/react'; -import { newsletterCard } from '../../../../fixtures/manual/highlights-trails'; +import { newsletterSignupCard } from '../../../../fixtures/manual/highlights-trails'; +import { sendNewsletterSignupEvent } from '../../../lib/newsletterSignupTracking'; import { useNewsletterSubscription } from '../../../lib/useNewsletterSubscription'; import { ConfigProvider } from '../../ConfigContext'; import { HighlightsNewsletterSignupModal } from './HighlightsNewsletterSignupModal'; @@ -8,6 +9,14 @@ import { HighlightsNewsletterSignupModal } from './HighlightsNewsletterSignupMod // Only mock what cannot run in jsdom: the signup form (reCAPTCHA, identity // API) and the subscription hook (network + auth). +jest.mock('../../../lib/newsletterSignupTracking', () => ({ + sendNewsletterSignupEvent: jest.fn(), + NEWSLETTER_SIGNUP_COMPONENT_ID: { + highlightsCard: (id: string) => `highlights-card-${id}`, + highlightsModal: (id: string) => `highlights-modal-${id}`, + }, +})); + jest.mock('../../../lib/useNewsletterSubscription', () => ({ useNewsletterSubscription: jest.fn(), })); @@ -28,7 +37,9 @@ const mockUseNewsletterSubscription = typeof useNewsletterSubscription >; -const newsletter = newsletterCard.newsletterData!; +const newsletter = newsletterSignupCard.newsletterData!; +// Mirrors the mock factory above +const componentId = `highlights-card-${newsletter.identityName}`; const renderModal = (onClose = jest.fn()) => render( @@ -43,6 +54,8 @@ const renderModal = (onClose = jest.fn()) => , ); @@ -58,6 +71,20 @@ describe('HighlightsNewsletterSignupModal', () => { jest.useRealTimers(); }); + it('fires a VIEW event on mount with the correct identifiers', () => { + renderModal(); + + expect(sendNewsletterSignupEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'VIEW', + identityName: newsletter.identityName, + componentId, + renderingTarget: 'Web', + value: { eventDescription: 'highlights-card-modal-viewed' }, + }), + ); + }); + it('renders a labelled dialog with the newsletter name', () => { renderModal(); diff --git a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.tsx b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.tsx index b13592b4cf9..81b31804723 100644 --- a/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.tsx +++ b/dotcom-rendering/src/components/Masthead/Newsletter/HighlightsNewsletterSignupModal.tsx @@ -5,15 +5,20 @@ import { space, } from '@guardian/source/foundations'; import { Button, SvgCross } from '@guardian/source/react-components'; -import { useId } from 'react'; +import { useEffect, useId } from 'react'; import { ArticleDesign, ArticleDisplay, Pillar, } from '../../../lib/articleFormat'; import { generateImageURL } from '../../../lib/image'; +import { + NEWSLETTER_SIGNUP_COMPONENT_ID, + sendNewsletterSignupEvent, +} from '../../../lib/newsletterSignupTracking'; import { useNewsletterSubscription } from '../../../lib/useNewsletterSubscription'; import type { Newsletter } from '../../../types/content'; +import type { RenderingTarget } from '../../../types/renderingTarget'; import { FormatBoundary } from '../../FormatBoundary'; import { ModalOverlay, useModalRequestClose } from '../../ModalOverlay'; import { NewsletterSignupCard } from '../../NewsletterSignupCard'; @@ -97,6 +102,8 @@ const visuallyHiddenStyles = css` type Props = { newsletter: Newsletter; onClose: () => void; + renderingTarget: RenderingTarget; + componentId: string; }; const HighlightsNewsletterSignupModalContent = ({ @@ -143,10 +150,9 @@ const HighlightsNewsletterSignupModalContent = ({ frequency={newsletter.frequency} isModal={true} isAlreadySubscribed={isSubscribed === true} - abTest={{ - name: 'highlights-newsletter-card', - variant: 'highlightsCard', - }} + componentId={NEWSLETTER_SIGNUP_COMPONENT_ID.highlightsModal( + newsletter.identityName, + )} /> @@ -156,6 +162,8 @@ const HighlightsNewsletterSignupModalContent = ({ export const HighlightsNewsletterSignupModal = ({ newsletter, onClose, + renderingTarget, + componentId, }: Props) => { const isSubscribed = useNewsletterSubscription( newsletter.listId, @@ -164,6 +172,18 @@ export const HighlightsNewsletterSignupModal = ({ const titleId = useId(); + useEffect(() => { + sendNewsletterSignupEvent({ + action: 'VIEW', + identityName: newsletter.identityName, + componentId, + renderingTarget, + value: { eventDescription: 'highlights-card-modal-viewed' }, + }); + // Fire once on mount only + // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally fire only on mount + }, []); + return ( { * * (No visual story exists as this does not render anything) */ -export const Metrics = ({ commercialMetricsEnabled, tests }: Props) => { +export const Metrics = ({ commercialMetricsEnabled }: Props) => { const abTests = useAB(); const adBlockerInUse = useAdBlockInUse(); const detectedAdBlocker = useDetectAdBlock(); @@ -77,8 +75,6 @@ export const Metrics = ({ commercialMetricsEnabled, tests }: Props) => { const isDev = useDev(); - const userInServerSideTest = Object.keys(tests).length > 0; - const userParticipations = abTests?.getParticipations() ?? {}; const collectTestMetrics = shouldCollectMetricsForTests( @@ -86,12 +82,8 @@ export const Metrics = ({ commercialMetricsEnabled, tests }: Props) => { ); const shouldBypassSampling = useCallback( - () => - willRecordCoreWebVitals || - userInServerSideTest || - collectTestMetrics, - - [userInServerSideTest, collectTestMetrics], + () => willRecordCoreWebVitals || collectTestMetrics, + [collectTestMetrics], ); useEffect( diff --git a/dotcom-rendering/src/components/MiniProfiles.stories.tsx b/dotcom-rendering/src/components/MiniProfiles.stories.tsx index 90885a83614..552e07a0b2f 100644 --- a/dotcom-rendering/src/components/MiniProfiles.stories.tsx +++ b/dotcom-rendering/src/components/MiniProfiles.stories.tsx @@ -59,7 +59,6 @@ export const ThemeVariations = meta.story({ display: ArticleDisplay.Standard, theme: Pillar.News, }, - abTests: {}, /** * This is used for rich links. An empty string isn't technically valid, * but there are no rich links in this example. diff --git a/dotcom-rendering/src/components/MiniProfiles.tsx b/dotcom-rendering/src/components/MiniProfiles.tsx index 8ba0eacb641..0c6c0f0a20f 100644 --- a/dotcom-rendering/src/components/MiniProfiles.tsx +++ b/dotcom-rendering/src/components/MiniProfiles.tsx @@ -3,7 +3,7 @@ import type { ArticleFormat } from '../lib/articleFormat'; import type { EditionId } from '../lib/edition'; import type { ArticleElementRenderer } from '../lib/renderElement'; import { palette } from '../palette'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { MiniProfile, StarRating } from '../types/content'; import { MiniProfile as MiniProfileComponent } from './MiniProfile'; @@ -14,7 +14,6 @@ interface MiniProfilesProps { pageId: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; editionId: EditionId; hideCaption?: boolean; @@ -45,7 +44,6 @@ export const MiniProfiles = ({ isAdFreeUser, isSensitive, switches, - abTests, editionId, hideCaption, starRating, @@ -79,7 +77,6 @@ export const MiniProfiles = ({ isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} switches={switches} - abTests={abTests} editionId={editionId} hideCaption={hideCaption} starRating={starRating} diff --git a/dotcom-rendering/src/components/ModalOverlay.test.tsx b/dotcom-rendering/src/components/ModalOverlay.test.tsx index cac148c2595..11f0703c744 100644 --- a/dotcom-rendering/src/components/ModalOverlay.test.tsx +++ b/dotcom-rendering/src/components/ModalOverlay.test.tsx @@ -48,12 +48,12 @@ describe('ModalOverlay', () => { expect(onClose).toHaveBeenCalledTimes(1); }); - it('calls onClose when the overlay backdrop receives a mousedown', () => { + it('calls onClose when the overlay backdrop receives a pointerdown', () => { const onClose = jest.fn(); renderOverlay(onClose); const overlay = screen.getByRole('dialog').parentElement!; - fireEvent.mouseDown(overlay); + fireEvent.pointerDown(overlay); act(() => { jest.runAllTimers(); @@ -62,11 +62,11 @@ describe('ModalOverlay', () => { expect(onClose).toHaveBeenCalledTimes(1); }); - it('does not call onClose when the dialog itself receives a mousedown', () => { + it('does not call onClose when the dialog itself receives a pointerdown', () => { const onClose = jest.fn(); renderOverlay(onClose); - fireEvent.mouseDown(screen.getByRole('dialog')); + fireEvent.pointerDown(screen.getByRole('dialog')); act(() => { jest.runAllTimers(); diff --git a/dotcom-rendering/src/components/ModalOverlay.tsx b/dotcom-rendering/src/components/ModalOverlay.tsx index b6248e89acd..f72d5b68efb 100644 --- a/dotcom-rendering/src/components/ModalOverlay.tsx +++ b/dotcom-rendering/src/components/ModalOverlay.tsx @@ -14,6 +14,10 @@ import { getZIndex } from '../lib/getZIndex'; const OPEN_ANIMATION_DURATION_MS = 300; const CLOSE_ANIMATION_DURATION_MS = 225; +const prefersReducedMotion = (): boolean => + typeof window !== 'undefined' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const ModalRequestCloseContext = createContext<(() => void) | null>(null); export const useModalRequestClose = (): (() => void) => { @@ -130,19 +134,15 @@ export const ModalOverlay = ({ const overlayRef = useRef(null); const dialogRef = useRef(null); const closeTimeoutRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); + + const [isVisible, setIsVisible] = useState(() => prefersReducedMotion()); const requestClose = useCallback(() => { if (closeTimeoutRef.current !== null) { return; } - const prefersReducedMotion = - window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? - false; - - if (prefersReducedMotion) { - closeTimeoutRef.current = 0; + if (prefersReducedMotion()) { onClose(); return; } @@ -156,12 +156,7 @@ export const ModalOverlay = ({ // Trigger open animation on mount useEffect(() => { - const prefersReducedMotion = - window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? - false; - - if (prefersReducedMotion) { - setIsVisible(true); + if (prefersReducedMotion()) { return; } @@ -205,7 +200,9 @@ export const ModalOverlay = ({ ? document.activeElement : null; - dialogElement.focus(); + // preventScroll stops iOS Safari from jerking the viewport to bring + // the off-screen element into view before the slide-up animation runs. + dialogElement.focus({ preventScroll: true }); return () => { if ( @@ -282,18 +279,22 @@ export const ModalOverlay = ({ return; } - const handleOverlayMouseDown = (event: MouseEvent) => { + const handleOverlayPointerDown = (event: PointerEvent) => { if (event.target === overlayElement) { + event.preventDefault(); requestClose(); } }; - overlayElement.addEventListener('mousedown', handleOverlayMouseDown); + overlayElement.addEventListener( + 'pointerdown', + handleOverlayPointerDown, + ); return () => { overlayElement.removeEventListener( - 'mousedown', - handleOverlayMouseDown, + 'pointerdown', + handleOverlayPointerDown, ); }; }, [requestClose]); @@ -305,7 +306,6 @@ export const ModalOverlay = ({ return createPortal(
    { const defaultProps: CardProps = { @@ -189,7 +190,7 @@ const getDefaultCardProps = ( branding: trail.branding, serverTime, imageLoading: 'lazy', - trailText: trail.trailText, + trailText: showTrailText ? trail.trailText : undefined, showAge: false, showTopBarDesktop: false, showTopBarMobile: false, @@ -238,6 +239,7 @@ export const MoreGalleries = (props: Props) => { firstTrail, props.discussionApiUrl, props.format, + true, props.serverTime, )} /> @@ -261,6 +263,7 @@ export const MoreGalleries = (props: Props) => { trail, props.discussionApiUrl, props.format, + false, props.serverTime, )} mediaSize="medium" diff --git a/dotcom-rendering/src/components/MultiBylines.stories.tsx b/dotcom-rendering/src/components/MultiBylines.stories.tsx index 9a7b9abf6c4..bb7b58a551d 100644 --- a/dotcom-rendering/src/components/MultiBylines.stories.tsx +++ b/dotcom-rendering/src/components/MultiBylines.stories.tsx @@ -71,7 +71,6 @@ export const ThemeVariations = meta.story({ display: ArticleDisplay.Standard, theme: Pillar.News, }, - abTests: {}, /** * This is used for rich links. An empty string isn't technically valid, * but there are no rich links in this example. diff --git a/dotcom-rendering/src/components/MultiBylines.tsx b/dotcom-rendering/src/components/MultiBylines.tsx index cb006374c3a..d9c53d7bce1 100644 --- a/dotcom-rendering/src/components/MultiBylines.tsx +++ b/dotcom-rendering/src/components/MultiBylines.tsx @@ -3,7 +3,7 @@ import type { ArticleFormat } from '../lib/articleFormat'; import type { EditionId } from '../lib/edition'; import type { ArticleElementRenderer } from '../lib/renderElement'; import { palette } from '../palette'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { MultiByline as MultiBylineModel, StarRating, @@ -17,7 +17,6 @@ interface MultiBylineProps { pageId: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; editionId: EditionId; hideCaption?: boolean; @@ -47,7 +46,6 @@ export const MultiBylines = ({ isAdFreeUser, isSensitive, switches, - abTests, editionId, hideCaption, starRating, @@ -77,7 +75,6 @@ export const MultiBylines = ({ isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} switches={switches} - abTests={abTests} editionId={editionId} hideCaption={hideCaption} starRating={starRating} diff --git a/dotcom-rendering/src/components/NewsletterPrivacyMessage.stories.tsx b/dotcom-rendering/src/components/NewsletterPrivacyMessage.stories.tsx index 9e029d3e2f2..925c668f3a9 100644 --- a/dotcom-rendering/src/components/NewsletterPrivacyMessage.stories.tsx +++ b/dotcom-rendering/src/components/NewsletterPrivacyMessage.stories.tsx @@ -10,3 +10,9 @@ export const Default = () => { }; Default.storyName = 'Default'; + +export const SignedIn = () => { + return ; +}; + +SignedIn.storyName = 'Signed in'; diff --git a/dotcom-rendering/src/components/NewsletterPrivacyMessage.tsx b/dotcom-rendering/src/components/NewsletterPrivacyMessage.tsx index 4280c7f7df4..2cbb74260b1 100644 --- a/dotcom-rendering/src/components/NewsletterPrivacyMessage.tsx +++ b/dotcom-rendering/src/components/NewsletterPrivacyMessage.tsx @@ -6,6 +6,7 @@ import { palette as themePalette } from '../palette'; interface Props { textColor?: 'supporting' | 'regular'; cssOverrides?: ReturnType; + isSignedIn?: boolean | 'Pending'; } const GUARDIAN_HOMEPAGE = 'https://www.theguardian.com'; @@ -72,15 +73,22 @@ const textStyles = (textColor: 'supporting' | 'regular') => { export const NewsletterPrivacyMessage = ({ textColor = 'supporting', cssOverrides, + isSignedIn, }: Props) => ( Privacy Notice: Newsletters may contain information about charities, online ads, and - content funded by outside parties. If you do not have an account, we - will create a guest account for you on{' '} - theguardian.com to send - you this newsletter. You can complete full registration at any time. For - more information about how we use your data see our{' '} + content funded by outside parties.{' '} + {isSignedIn !== true && ( + <> + If you do not have an account, we will create a guest account + for you on{' '} + theguardian.com{' '} + to send you this newsletter. You can complete full registration + at any time.{' '} + + )} + For more information about how we use your data see our{' '} Privacy Policy. ); diff --git a/dotcom-rendering/src/components/NewsletterSignupCardContainer.test.tsx b/dotcom-rendering/src/components/NewsletterSignupCardContainer.test.tsx index f8486e4aab4..d98347f41f9 100644 --- a/dotcom-rendering/src/components/NewsletterSignupCardContainer.test.tsx +++ b/dotcom-rendering/src/components/NewsletterSignupCardContainer.test.tsx @@ -73,7 +73,7 @@ describe('NewsletterSignupCardContainer', () => { expect.objectContaining({ component: { componentType: 'NEWSLETTER_SUBSCRIPTION', - id: NEWSLETTER_SIGNUP_COMPONENT_ID.variantIllustratedCard( + id: NEWSLETTER_SIGNUP_COMPONENT_ID.inArticleSignupForm( defaultProps.identityName, ), }, @@ -102,7 +102,7 @@ describe('NewsletterSignupCardContainer', () => { expect.objectContaining({ component: { componentType: 'NEWSLETTER_SUBSCRIPTION', - id: NEWSLETTER_SIGNUP_COMPONENT_ID.variantIllustratedCard( + id: NEWSLETTER_SIGNUP_COMPONENT_ID.inArticleSignupForm( defaultProps.identityName, ), }, @@ -176,7 +176,7 @@ describe('NewsletterSignupCardContainer', () => { expect.objectContaining({ action: 'EXPAND', component: expect.objectContaining({ - id: NEWSLETTER_SIGNUP_COMPONENT_ID.variantIllustratedCard( + id: NEWSLETTER_SIGNUP_COMPONENT_ID.inArticleSignupForm( defaultProps.identityName, ), }), diff --git a/dotcom-rendering/src/components/NewsletterSignupCardContainer.tsx b/dotcom-rendering/src/components/NewsletterSignupCardContainer.tsx index 47f7f08b7d8..1f374f7c93c 100644 --- a/dotcom-rendering/src/components/NewsletterSignupCardContainer.tsx +++ b/dotcom-rendering/src/components/NewsletterSignupCardContainer.tsx @@ -9,7 +9,6 @@ import { import type { RenderingTarget } from '../types/renderingTarget'; import type { NewsletterPreviewAction } from './NewsletterPreviewButton'; import { NewsletterPreviewModal } from './NewsletterPreviewModal'; -import { NewsletterPrivacyMessage } from './NewsletterPrivacyMessage'; import type { NewsletterSignupCardProps } from './NewsletterSignupCard'; import { NewsletterSignupCard } from './NewsletterSignupCard'; @@ -32,7 +31,7 @@ const sendPreviewTracking = ({ action: eventDescription === 'preview-open' ? 'EXPAND' : 'CLOSE', identityName, componentId: - NEWSLETTER_SIGNUP_COMPONENT_ID.variantIllustratedCard(identityName), + NEWSLETTER_SIGNUP_COMPONENT_ID.inArticleSignupForm(identityName), renderingTarget, value: { eventDescription, renderUrl, isSignedIn }, }); @@ -72,7 +71,6 @@ export const NewsletterSignupCardContainer = ({ children, isSignedIn, }: Props) => { - const showPrivacyMessageOutside = isSignedIn === true; const [isPreviewOpen, setIsPreviewOpen] = useState(false); const renderUrl = buildNewsletterPreviewUrl({ @@ -168,15 +166,6 @@ export const NewsletterSignupCardContainer = ({ > {children?.(previewAction)} - {showPrivacyMessageOutside && ( - - )}
    ); diff --git a/dotcom-rendering/src/components/NewsletterSignupForm.island.stories.tsx b/dotcom-rendering/src/components/NewsletterSignupForm.island.stories.tsx index d487056118b..392ed120914 100644 --- a/dotcom-rendering/src/components/NewsletterSignupForm.island.stories.tsx +++ b/dotcom-rendering/src/components/NewsletterSignupForm.island.stories.tsx @@ -50,6 +50,7 @@ const defaultArgs = { newsletterName: 'Saturday Edition', frequency: 'every Saturday', previewAction: { behaviour: 'modal' as const, onClick: fn() }, + componentId: 'AR NewsletterSignupForm saturday-edition', }; /** Shared no-op handlers — stories that focus on visual state don't need real callbacks. */ @@ -223,20 +224,6 @@ export const WithoutPreview = meta.story({ }, }); -/** `hidePrivacyMessage` — focused state without the privacy notice. */ -export const HidePrivacyMessage = meta.story({ - args: { ...defaultArgs, hidePrivacyMessage: true }, - beforeEach() { - mocked(useNewsletterSignupForm).mockReturnValue( - mockForm({ - isInteracted: true, - showMarketingToggle: true, - marketingOptIn: true, - }), - ); - }, -}); - /** User is already subscribed — success message shown immediately. */ export const AlreadySubscribed = meta.story({ args: { ...defaultArgs, isAlreadySubscribed: true }, diff --git a/dotcom-rendering/src/components/NewsletterSignupForm.island.test.tsx b/dotcom-rendering/src/components/NewsletterSignupForm.island.test.tsx index 5e49fb93df9..5ba3d0960d7 100644 --- a/dotcom-rendering/src/components/NewsletterSignupForm.island.test.tsx +++ b/dotcom-rendering/src/components/NewsletterSignupForm.island.test.tsx @@ -75,7 +75,7 @@ jest.mock('react-google-recaptcha', () => ({ ), })); -const renderForm = (hidePrivacyMessage = false) => +const renderForm = () => render( newsletterId="morning-briefing" newsletterName="Morning Briefing" frequency="every day" - hidePrivacyMessage={hidePrivacyMessage} + componentId="AR NewsletterSignupForm morning-briefing" /> , ); @@ -102,7 +102,14 @@ const getTrackedEventDescription = (call: unknown[]): string => { const getTrackedPayloadForEvent = ( eventDescription: string, -): { eventDescription: string; marketingOptInType?: string } | undefined => { +): + | { + eventDescription: string; + marketingOptInType?: string; + responseStatus?: number; + errorType?: string; + } + | undefined => { const trackingCalls = (submitComponentEvent as jest.Mock).mock .calls as Array<[{ value: string }]>; @@ -110,6 +117,8 @@ const getTrackedPayloadForEvent = ( const parsed = JSON.parse(payload.value) as { eventDescription: string; marketingOptInType?: string; + responseStatus?: number; + errorType?: string; }; if (parsed.eventDescription === eventDescription) { @@ -232,7 +241,7 @@ describe('NewsletterSignupForm', () => { it('supports tab order from email input to marketing toggle to sign up', async () => { const testUser = user.setup(); - renderForm(true); + renderForm(); await testUser.tab(); expect(screen.getByLabelText('Enter your email')).toHaveFocus(); @@ -277,7 +286,7 @@ describe('NewsletterSignupForm', () => { it('supports keyboard interaction for marketing toggle and submit button', async () => { const testUser = user.setup(); - renderForm(true); + renderForm(); await testUser.tab(); const emailInput = screen.getByLabelText('Enter your email'); @@ -395,13 +404,14 @@ describe('NewsletterSignupForm', () => { ).not.toBeInTheDocument(); }); - it('shows failure UI with retry and supports hiding the privacy message', async () => { + it('shows failure UI with retry', async () => { const testUser = user.setup(); - global.fetch = jest.fn().mockResolvedValue({ ok: false } as Response); + global.fetch = jest + .fn() + .mockResolvedValue({ ok: false, status: 500 } as Response); - renderForm(true); + renderForm(); - expect(screen.queryByText('Privacy Notice:')).not.toBeInTheDocument(); await typeEmailAddress(testUser); await testUser.click(screen.getByRole('button', { name: 'Sign up' })); @@ -417,6 +427,9 @@ describe('NewsletterSignupForm', () => { 'form-submission', 'submission-failed', ]); + expect( + getTrackedPayloadForEvent('submission-failed')?.responseStatus, + ).toBe(500); await testUser.click(screen.getByRole('button', { name: 'Try again' })); @@ -490,6 +503,36 @@ describe('NewsletterSignupForm', () => { ).toBeInTheDocument(); }); + it('tracks an error type when the submit request throws', async () => { + const testUser = user.setup(); + global.fetch = jest + .fn() + .mockRejectedValue(new TypeError('Failed to fetch')); + + renderForm(); + + await typeEmailAddress(testUser); + await testUser.click(screen.getByRole('button', { name: 'Sign up' })); + + await waitFor(() => { + expect( + screen.getByText(/there was an error signing you up\./i), + ).toBeInTheDocument(); + }); + + expectTrackedEventDescriptions([ + 'email-input-focused', + 'click-button', + 'open-captcha', + 'captcha-passed', + 'form-submission', + 'form-submit-error', + ]); + expect(getTrackedPayloadForEvent('form-submit-error')?.errorType).toBe( + 'network-or-fetch', + ); + }); + describe('US hide marketing toggle (usSignupHideMarketingToggle switch)', () => { beforeEach(() => { window.guardian.config.switches['usSignupHideMarketingToggle'] = diff --git a/dotcom-rendering/src/components/NewsletterSignupForm.island.tsx b/dotcom-rendering/src/components/NewsletterSignupForm.island.tsx index 7ad0136b7ca..b39b046ab90 100644 --- a/dotcom-rendering/src/components/NewsletterSignupForm.island.tsx +++ b/dotcom-rendering/src/components/NewsletterSignupForm.island.tsx @@ -32,10 +32,12 @@ type Props = { newsletterId: string; newsletterName: string; frequency: string; - hidePrivacyMessage?: boolean; previewAction?: NewsletterPreviewAction; /** When `true`, the success message is shown immediately (user is already subscribed). */ isAlreadySubscribed?: boolean; + /** Ophan component ID for tracking events. Derived by the caller from the active + * component/experiment so that this form is not coupled to test state. */ + componentId: string; /** Ophan A/B test metadata — forwarded to tracking events. */ abTest?: AbTest; /** @@ -71,7 +73,6 @@ const signedOutLayoutStyles = css` const signedInLayoutStyles = css` grid-template-columns: minmax(0, 1fr); grid-template-areas: 'submit'; - padding-bottom: ${space[2]}px; `; const emailFieldStyles = css` @@ -114,9 +115,24 @@ const getToggleContainerStyles = (isFullWidth: boolean) => css` min-width: 0; `; -const privacyContainerStyles = css` - grid-column: 1 / -1; -`; +const getPrivacyContainerStyles = ( + isSignedIn: boolean | 'Pending', + isModal: boolean, +) => { + if (isSignedIn === true && !isModal) { + return css` + margin-top: ${space[5]}px; + padding-top: ${space[2]}px; + border-top: 1px solid ${palette('--card-border-supporting')}; + `; + } + return css` + margin-top: ${space[2]}px; + ${until.tablet} { + margin-top: ${space[3]}px; + } + `; +}; const successTextStyles = css` color: ${palette('--newsletter-card-description')}; @@ -276,8 +292,8 @@ const NewsletterSignupFormActive = ({ newsletterId, newsletterName, frequency, - hidePrivacyMessage = false, previewAction, + componentId, abTest, isModal = false, }: Omit) => { @@ -308,6 +324,7 @@ const NewsletterSignupFormActive = ({ } = useNewsletterSignupForm( newsletterId, renderingTarget, + componentId, abTest, hideMarketingToggle, ); @@ -357,28 +374,18 @@ const NewsletterSignupFormActive = ({ />
    )} - {showAdditionalFields && ( - <> - {showMarketingToggle && ( -
    -
    - -
    -
    - )} - {!hidePrivacyMessage && ( -
    - -
    - )} - + {showAdditionalFields && showMarketingToggle && ( +
    +
    + +
    +
    )}
    {isSignedIn && previewAction && ( @@ -400,6 +407,12 @@ const NewsletterSignupFormActive = ({
    + {showAdditionalFields && showForm && ( +
    + +
    + )} + {showSuccess && ( { await waitFor(() => @@ -67,7 +67,9 @@ export const NotificationsToggle = { await expect( mockNotificationsClient.follow, ).toHaveBeenCalledWith(expectedTopic); - await expect(button).toHaveTextContent('Notifications on'); + await expect(button).toHaveTextContent( + 'Live blog notifications on', + ); }); }); @@ -77,7 +79,9 @@ export const NotificationsToggle = { await expect( mockNotificationsClient.unfollow, ).toHaveBeenCalledWith(expectedTopic); - await expect(button).toHaveTextContent('Notifications off'); + await expect(button).toHaveTextContent( + 'Live blog notifications off', + ); }); }); }, diff --git a/dotcom-rendering/src/components/NotificationsToggle.tsx b/dotcom-rendering/src/components/NotificationsToggle.tsx index c934574bc5c..2ebe085c8bf 100644 --- a/dotcom-rendering/src/components/NotificationsToggle.tsx +++ b/dotcom-rendering/src/components/NotificationsToggle.tsx @@ -51,7 +51,8 @@ export const NotificationsToggle = (props: Props) => { )} className={props.className} > - Notifications {isFollowing ? 'on' : 'off'} + + {isFollowing ? ' on' : ' off'} ); }; @@ -142,3 +143,14 @@ const handleError = error, ); }; + +const Description = (props: { + notificationType: Props['notificationType']; +}) => { + switch (props.notificationType) { + case 'content': + return 'Live blog notifications'; + case 'football-match': + return 'Live match updates'; + } +}; diff --git a/dotcom-rendering/src/components/OnwardsUpper.island.tsx b/dotcom-rendering/src/components/OnwardsUpper.island.tsx index 1dca2e1c4ea..7f6cb674881 100644 --- a/dotcom-rendering/src/components/OnwardsUpper.island.tsx +++ b/dotcom-rendering/src/components/OnwardsUpper.island.tsx @@ -318,7 +318,9 @@ export const OnwardsUpper = ({ const showCuratedContainer = !!curatedDataUrl && !isPaidContent && canHaveCuratedContent; - const isGalleryArticle = format.design === ArticleDesign.Gallery; + const isGalleryArticle = + format.design === ArticleDesign.Gallery || + format.design === ArticleDesign.HostedGallery; return (
    diff --git a/dotcom-rendering/src/components/Pagination.test.tsx b/dotcom-rendering/src/components/Pagination.test.tsx new file mode 100644 index 00000000000..051c3221497 --- /dev/null +++ b/dotcom-rendering/src/components/Pagination.test.tsx @@ -0,0 +1,64 @@ +import { render } from '@testing-library/react'; +import { Pagination } from './Pagination'; + +describe('Pagination', () => { + describe('newer and newest', () => { + it('does not render the newer/newest controls when the props are undefined', () => { + const { queryAllByText, queryByText } = render( + , + ); + + expect(queryAllByText('Newest').length).toBe(0); + expect(queryByText('Previous')).toBeNull(); + }); + + it('renders the newer/newest controls when the props are empty strings', () => { + const { queryAllByText, queryByText } = render( + , + ); + + expect(queryAllByText('Newest').length).toBeGreaterThan(0); + expect(queryByText('Previous')).not.toBeNull(); + }); + }); + + describe('older and oldest', () => { + it('does not render the older/oldest controls when the props are undefined', () => { + const { queryAllByText, queryByText } = render( + , + ); + + expect(queryAllByText('Oldest').length).toBe(0); + expect(queryByText('Next')).toBeNull(); + }); + + it('renders the older/oldest controls when the props are empty strings', () => { + const { queryAllByText, queryByText } = render( + , + ); + + expect(queryAllByText('Oldest').length).toBeGreaterThan(0); + expect(queryByText('Next')).not.toBeNull(); + }); + }); +}); diff --git a/dotcom-rendering/src/components/Pagination.tsx b/dotcom-rendering/src/components/Pagination.tsx index 5baf7bb3fac..793ba3c2eb2 100644 --- a/dotcom-rendering/src/components/Pagination.tsx +++ b/dotcom-rendering/src/components/Pagination.tsx @@ -93,7 +93,7 @@ export const Pagination = ({ style={{ gridArea: 'newer', justifySelf: 'start' }} css={flexSection} > - {!!newest && ( + {newest !== undefined && ( <> )} - {!!newer && ( + {newer !== undefined && ( - {!!older && ( + {older !== undefined && ( )} - {!!oldest && ( + {oldest !== undefined && ( <> { }; const styles = ({ design }: ArticleFormat, isLightbox: boolean) => { - if (design === ArticleDesign.Gallery) { + if ( + design === ArticleDesign.Gallery || + design === ArticleDesign.HostedGallery + ) { return css` img { width: 100%; diff --git a/dotcom-rendering/src/components/PinnedPost.stories.tsx b/dotcom-rendering/src/components/PinnedPost.stories.tsx index c0a6763d3c7..d5292db1949 100644 --- a/dotcom-rendering/src/components/PinnedPost.stories.tsx +++ b/dotcom-rendering/src/components/PinnedPost.stories.tsx @@ -103,7 +103,6 @@ export const Sport = { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={true} editionId={'UK'} @@ -137,7 +136,6 @@ export const News = { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={true} editionId={'UK'} @@ -171,7 +169,6 @@ export const Culture = { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={true} editionId={'UK'} @@ -205,7 +202,6 @@ export const Lifestyle = { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={true} editionId={'UK'} @@ -239,7 +235,6 @@ export const Opinion = { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={true} editionId={'UK'} @@ -273,7 +268,6 @@ export const SpecialReport = { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={true} editionId={'UK'} @@ -307,7 +301,6 @@ export const Labs = { ajaxUrl="" isAdFreeUser={false} isSensitive={false} - abTests={{}} switches={{}} isPinnedPost={true} editionId={'UK'} diff --git a/dotcom-rendering/src/components/PinnedPost.tsx b/dotcom-rendering/src/components/PinnedPost.tsx index de212029444..844ea5b3b02 100644 --- a/dotcom-rendering/src/components/PinnedPost.tsx +++ b/dotcom-rendering/src/components/PinnedPost.tsx @@ -12,9 +12,9 @@ import { visuallyHidden, } from '@guardian/source/foundations'; import { - SvgMinus, + SvgChevronDownSingle, + SvgChevronUpSingle, SvgPinned, - SvgPlus, } from '@guardian/source/react-components'; import { palette } from '../palette'; import type { Block } from '../types/blocks'; @@ -36,12 +36,12 @@ const pinnedPostContainer = css` margin-bottom: ${space[1]}px; } #pinned-post-checkbox:checked ~ #pinned-post-overlay, - #pinned-post-checkbox ~ label #svgminus, - #pinned-post-checkbox:checked ~ label #svgplus { + #pinned-post-checkbox ~ label #svgchevronupsingle, + #pinned-post-checkbox:checked ~ label #svgchevrondownsingle { display: none; } - #pinned-post-checkbox ~ label #svgplus, - #pinned-post-checkbox:checked ~ label #svgminus { + #pinned-post-checkbox ~ label #svgchevrondownsingle, + #pinned-post-checkbox:checked ~ label #svgchevronupsingle { display: block; } #pinned-post-checkbox ~ label::after { @@ -188,11 +188,11 @@ export const PinnedPost = ({ pinnedPost, children, serverTime }: Props) => { id="pinned-post-button" > <> - - + + - - + + diff --git a/dotcom-rendering/src/components/ProductElement.stories.tsx b/dotcom-rendering/src/components/ProductElement.stories.tsx index 04657ed06dd..6e6a6186cb6 100644 --- a/dotcom-rendering/src/components/ProductElement.stories.tsx +++ b/dotcom-rendering/src/components/ProductElement.stories.tsx @@ -9,7 +9,6 @@ import type { ProductBlockElement } from '../types/content'; import { ProductElement } from './ProductElement'; const ArticleElementComponent = getNestedArticleElement({ - abTests: {}, ajaxUrl: '', editionId: 'UK', isAdFreeUser: false, @@ -234,6 +233,7 @@ const product = { label: '£79.99 at John Lewis', linkType: 'ProductButton', priority: 'Primary', + elementId: '123', }, { _type: 'model.dotcomrendering.pageElements.LinkBlockElement', @@ -241,6 +241,7 @@ const product = { label: '£79.99 at Amazon', linkType: 'ProductButton', priority: 'Primary', + elementId: '1234', }, { _type: 'model.dotcomrendering.pageElements.TextBlockElement', diff --git a/dotcom-rendering/src/components/QAndAExplainer.tsx b/dotcom-rendering/src/components/QAndAExplainer.tsx index 607661383ca..752cb9cfb17 100644 --- a/dotcom-rendering/src/components/QAndAExplainer.tsx +++ b/dotcom-rendering/src/components/QAndAExplainer.tsx @@ -4,7 +4,7 @@ import type { EditionId } from '../lib/edition'; import type { ArticleElementRenderer } from '../lib/renderElement'; import { slugify } from '../model/enhance-H2s'; import { palette } from '../palette'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { QAndAExplainer as QAndAExplainerModel, StarRating, @@ -19,7 +19,6 @@ interface Props { pageId: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; editionId: EditionId; hideCaption?: boolean; @@ -45,7 +44,6 @@ export const QAndAExplainer = ({ isAdFreeUser, isSensitive, switches, - abTests, editionId, hideCaption, starRating, @@ -76,7 +74,6 @@ export const QAndAExplainer = ({ isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} switches={switches} - abTests={abTests} editionId={editionId} hideCaption={hideCaption} starRating={starRating} diff --git a/dotcom-rendering/src/components/QAndAExplainers.stories.tsx b/dotcom-rendering/src/components/QAndAExplainers.stories.tsx index b6f7a9af594..d9f9c504b5d 100644 --- a/dotcom-rendering/src/components/QAndAExplainers.stories.tsx +++ b/dotcom-rendering/src/components/QAndAExplainers.stories.tsx @@ -47,7 +47,6 @@ export const AllThemes = meta.story({ display: ArticleDisplay.Standard, theme: Pillar.News, }, - abTests: {}, /** * This is used for rich links. An empty string isn't technically valid, * but there are no rich links in this example. diff --git a/dotcom-rendering/src/components/QAndAExplainers.tsx b/dotcom-rendering/src/components/QAndAExplainers.tsx index 99ba9d8e8ba..3a5908fc9e8 100644 --- a/dotcom-rendering/src/components/QAndAExplainers.tsx +++ b/dotcom-rendering/src/components/QAndAExplainers.tsx @@ -3,7 +3,7 @@ import type { ArticleFormat } from '../lib/articleFormat'; import type { EditionId } from '../lib/edition'; import type { ArticleElementRenderer } from '../lib/renderElement'; import { palette } from '../palette'; -import type { ServerSideTests, Switches } from '../types/config'; +import type { Switches } from '../types/config'; import type { QAndAExplainer, StarRating } from '../types/content'; import { QAndAExplainer as QAndAExplainerComponent } from './QAndAExplainer'; @@ -15,7 +15,6 @@ interface Props { pageId: string; isAdFreeUser: boolean; isSensitive: boolean; - abTests: ServerSideTests; switches: Switches; editionId: EditionId; hideCaption?: boolean; @@ -44,7 +43,6 @@ export const QAndAExplainers = ({ isAdFreeUser, isSensitive, switches, - abTests, editionId, hideCaption, starRating, @@ -63,7 +61,6 @@ export const QAndAExplainers = ({ isAdFreeUser={isAdFreeUser} isSensitive={isSensitive} switches={switches} - abTests={abTests} editionId={editionId} hideCaption={hideCaption} starRating={starRating} diff --git a/dotcom-rendering/src/components/ScrollableHighlights.island.test.tsx b/dotcom-rendering/src/components/ScrollableHighlights.island.test.tsx index b1801abd9a3..b36ebc67af5 100644 --- a/dotcom-rendering/src/components/ScrollableHighlights.island.test.tsx +++ b/dotcom-rendering/src/components/ScrollableHighlights.island.test.tsx @@ -2,7 +2,7 @@ import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import { defaultCard, - newsletterCard, + newsletterSignupCard, trails, } from '../../fixtures/manual/highlights-trails'; import { ConfigProvider } from './ConfigContext'; @@ -12,12 +12,12 @@ jest.mock('../lib/newsletterSignupTracking', () => ({ sendNewsletterSignupEvent: jest.fn(), NEWSLETTER_SIGNUP_COMPONENT_ID: { highlightsCard: (id: string) => `highlights-card-${id}`, + highlightsModal: (id: string) => `highlights-modal-${id}`, }, })); const renderHighlights = ( trailList: React.ComponentProps['trails'], - isNewsletterSignupCardEnabled: boolean, ) => render( - + , ); -const newsletterCardWithoutData = { - ...newsletterCard, +/** A signup card where newsletterData is missing — should be hidden. */ +const newsletterSignupCardWithoutData = { + ...newsletterSignupCard, newsletterData: undefined, }; -describe('ScrollableHighlights — newsletter card AB test', () => { +/** An article tagged with a newsletter topic but NOT a signup card — should always be shown. */ +const newsletterTaggedArticle = { + ...newsletterSignupCard, + isNewsletter: true, + isNewsletterSignup: false, + newsletterData: undefined, +}; + +describe('ScrollableHighlights', () => { beforeEach(() => { jest.clearAllMocks(); }); - describe('when user is in the "enable" group', () => { - it('renders the HighlightsNewsletterCard for a newsletter trail', () => { - renderHighlights([newsletterCard], true); - - expect( - screen.getByRole('link', { - name: newsletterCard.headline, - }), - ).toBeInTheDocument(); - }); - - it('does not render a newsletter card when newsletterData is missing', () => { - renderHighlights([newsletterCardWithoutData], true); - - expect( - screen.queryByRole('link', { - name: newsletterCardWithoutData.headline, - }), - ).not.toBeInTheDocument(); - }); - - it('still renders regular cards alongside the newsletter card', () => { - renderHighlights([newsletterCard, defaultCard], true); + it('renders the HighlightsNewsletterCard for a newsletter signup trail', () => { + renderHighlights([newsletterSignupCard]); - expect( - screen.getByRole('link', { - name: `${newsletterCard.headline}`, - }), - ).toBeInTheDocument(); - expect(screen.getByText(defaultCard.headline)).toBeInTheDocument(); - }); + expect( + screen.getByRole('link', { + name: newsletterSignupCard.headline, + }), + ).toBeInTheDocument(); }); - describe('when user is NOT in the "enable" group', () => { - it('does not render a newsletter card when newsletterData is missing', () => { - renderHighlights([newsletterCardWithoutData], false); + it('does not render a newsletter signup card when newsletterData is missing', () => { + renderHighlights([newsletterSignupCardWithoutData]); - expect( - screen.queryByRole('link', { - name: newsletterCardWithoutData.headline, - }), - ).not.toBeInTheDocument(); - }); - - it('does not render a newsletter card at all', () => { - renderHighlights([newsletterCard], false); - - expect( - screen.queryByRole('link', { - name: newsletterCard.headline, - }), - ).not.toBeInTheDocument(); - - expect( - screen.queryByText('Free newsletter'), - ).not.toBeInTheDocument(); - }); + expect( + screen.queryByRole('link', { + name: newsletterSignupCardWithoutData.headline, + }), + ).not.toBeInTheDocument(); + }); - it('does not render a newsletter trail when newsletterData is missing', () => { - renderHighlights([newsletterCardWithoutData], false); + it('renders a newsletter-tagged article (isNewsletter) even when newsletterData is missing', () => { + renderHighlights([newsletterTaggedArticle]); - expect( - screen.queryByRole('link', { - name: newsletterCardWithoutData.headline, - }), - ).not.toBeInTheDocument(); - expect( - screen.queryByText('Free newsletter'), - ).not.toBeInTheDocument(); - }); + expect( + screen.getByRole('link', { + name: newsletterTaggedArticle.headline, + }), + ).toBeInTheDocument(); + }); - it('still renders non-newsletter cards normally', () => { - renderHighlights([newsletterCard, defaultCard], false); + it('still renders regular cards alongside the newsletter card', () => { + renderHighlights([newsletterSignupCard, defaultCard]); - expect(screen.getByText(defaultCard.headline)).toBeInTheDocument(); - }); + expect( + screen.getByRole('link', { + name: newsletterSignupCard.headline, + }), + ).toBeInTheDocument(); + expect(screen.getByText(defaultCard.headline)).toBeInTheDocument(); + }); - it('renders all regular trails unaffected', () => { - renderHighlights(trails.slice(0, 3), false); + it('renders all regular trails correctly', () => { + renderHighlights(trails.slice(0, 3)); - expect(screen.getByText(trails[0]!.headline)).toBeInTheDocument(); - expect(screen.getByText(trails[1]!.headline)).toBeInTheDocument(); - expect(screen.getByText(trails[2]!.headline)).toBeInTheDocument(); - }); + expect(screen.getByText(trails[0]!.headline)).toBeInTheDocument(); + expect(screen.getByText(trails[1]!.headline)).toBeInTheDocument(); + expect(screen.getByText(trails[2]!.headline)).toBeInTheDocument(); }); }); diff --git a/dotcom-rendering/src/components/ScrollableHighlights.island.tsx b/dotcom-rendering/src/components/ScrollableHighlights.island.tsx index b7b0f8bef1a..e98a44673ed 100644 --- a/dotcom-rendering/src/components/ScrollableHighlights.island.tsx +++ b/dotcom-rendering/src/components/ScrollableHighlights.island.tsx @@ -18,7 +18,6 @@ import { HighlightsNewsletterCard } from './Masthead/Newsletter/HighlightsNewsle type Props = { trails: DCRFrontCard[]; frontId?: string; - isNewsletterSignupCardEnabled: boolean; }; const containerStyles = css` @@ -212,16 +211,12 @@ const getOphanInfo = (frontId?: string) => { }; }; -export const ScrollableHighlights = ({ - trails, - frontId, - isNewsletterSignupCardEnabled, -}: Props) => { +export const ScrollableHighlights = ({ trails, frontId }: Props) => { const carouselRef = useRef(null); const visibleTrails = trails.filter((trail) => { - if (trail.isNewsletter !== true) return true; - return isNewsletterSignupCardEnabled && Boolean(trail.newsletterData); + if (trail.isNewsletterSignup !== true) return true; + return Boolean(trail.newsletterData); }); const carouselLength = visibleTrails.length; const imageLoading = 'eager'; diff --git a/dotcom-rendering/src/components/ScrollableHighlights.stories.tsx b/dotcom-rendering/src/components/ScrollableHighlights.stories.tsx index b2e630d5003..0d00e140419 100644 --- a/dotcom-rendering/src/components/ScrollableHighlights.stories.tsx +++ b/dotcom-rendering/src/components/ScrollableHighlights.stories.tsx @@ -2,7 +2,7 @@ import { breakpoints } from '@guardian/source/foundations'; import preview from '../../.storybook/preview'; import { defaultCard, - newsletterCard, + newsletterSignupCard, trails, } from '../../fixtures/manual/highlights-trails'; import { ScrollableHighlights } from './ScrollableHighlights.island'; @@ -39,7 +39,6 @@ const meta = preview.meta({ export const Default = meta.story({ args: { trails: trails.slice(0, 6), - isNewsletterSignupCardEnabled: false, }, }); @@ -47,7 +46,6 @@ export const withEightCards = meta.story({ name: 'With Eight Cards', args: { trails: trails.slice(0, 8), - isNewsletterSignupCardEnabled: false, }, }); @@ -62,7 +60,6 @@ export const withTwoLineKicker = meta.story({ }, ...Default.input.args.trails, ], - isNewsletterSignupCardEnabled: false, }, }); @@ -78,7 +75,6 @@ export const withLiveKicker = meta.story({ }, ...Default.input.args.trails, ], - isNewsletterSignupCardEnabled: false, }, }); @@ -94,7 +90,6 @@ export const withFourLineHeadline = meta.story({ }, ...Default.input.args.trails, ], - isNewsletterSignupCardEnabled: false, }, }); @@ -110,15 +105,13 @@ export const withExcessivleyLongHeadline = meta.story({ }, ...Default.input.args.trails, ], - isNewsletterSignupCardEnabled: false, }, }); export const withNewsletterCardVariant = meta.story({ ...Default.input, - name: 'With Newsletter Signup Card (AB enabled)', + name: 'With Newsletter Signup Card', args: { - trails: [newsletterCard, ...Default.input.args.trails], - isNewsletterSignupCardEnabled: true, + trails: [newsletterSignupCard, ...Default.input.args.trails], }, }); diff --git a/dotcom-rendering/src/components/SecureSignup.island.test.tsx b/dotcom-rendering/src/components/SecureSignup.island.test.tsx index acac253ab99..60452076105 100644 --- a/dotcom-rendering/src/components/SecureSignup.island.test.tsx +++ b/dotcom-rendering/src/components/SecureSignup.island.test.tsx @@ -323,24 +323,12 @@ describe('SecureSignup tracking component id', () => { global.fetch = jest.fn().mockResolvedValue({ ok: true } as Response); }); - it('uses the control component id when no abTest variant is provided', async () => { - const testUser = user.setup(); - renderSecureSignup(); - await submitSignupForm(testUser); - - await waitFor(() => { - expect( - getTrackingCallForEvent('form-submission')?.componentId, - ).toBe('AR SecureSignup morning-briefing'); - }); - }); - - it('uses the variantNewField component id when variant is variantNewField', async () => { + it('always uses the secureSignup component id regardless of abTest variant', async () => { const testUser = user.setup(); renderSecureSignup({ abTest: { name: 'test-ab', - variant: 'variantNewField', + variant: 'someVariant', }, }); await submitSignupForm(testUser); @@ -348,7 +336,7 @@ describe('SecureSignup tracking component id', () => { await waitFor(() => { expect( getTrackingCallForEvent('form-submission')?.componentId, - ).toBe('AR SecureSignup morning-briefing - variantNewField'); + ).toBe('AR SecureSignup morning-briefing'); }); }); }); diff --git a/dotcom-rendering/src/components/SecureSignup.island.tsx b/dotcom-rendering/src/components/SecureSignup.island.tsx index ad2db25b31b..9fabe6cfc84 100644 --- a/dotcom-rendering/src/components/SecureSignup.island.tsx +++ b/dotcom-rendering/src/components/SecureSignup.island.tsx @@ -24,6 +24,10 @@ import { getEffectiveMarketingOptIn, getMarketingOptInType, } from '../lib/newsletter-marketing-opt-in'; +import { + getErrorType, + getResponseErrorDescription, +} from '../lib/newsletterSignupFailureDetails'; import { EVENT_DESCRIPTION_TO_ACTION, NEWSLETTER_SIGNUP_COMPONENT_ID, @@ -41,7 +45,7 @@ import { useConfig } from './ConfigContext'; // The Google documentation specifies that if the 'recaptcha-badge' is hidden, // their T+C's must be displayed instead. While this component hides the // badge, its parent must include the T+C along side it. -// The T+C are not included in this componet directly to reduce layout shift +// The T+C are not included in this component directly to reduce layout shift // from the island hydrating (placeholder height for the text can't // be accurately predicated for every breakpoint). // https://developers.google.com/recaptcha/docs/faq#id-like-to-hide-the-recaptcha-badge.-what-is-allowed @@ -245,7 +249,6 @@ const postFormData = async ( }, }); }; - const sendTracking = ( newsletterId: string, eventDescription: NewsletterEventDescription, @@ -254,15 +257,10 @@ const sendTracking = ( abTest?: AbTest, extraDetails?: Record, ): void => { - const componentId = - abTest?.variant === 'variantNewField' - ? NEWSLETTER_SIGNUP_COMPONENT_ID.variantNewField(newsletterId) - : NEWSLETTER_SIGNUP_COMPONENT_ID.control(newsletterId); - sendNewsletterSignupEvent({ action: EVENT_DESCRIPTION_TO_ACTION[eventDescription], identityName: newsletterId, - componentId, + componentId: NEWSLETTER_SIGNUP_COMPONENT_ID.secureSignup(newsletterId), renderingTarget, value: { ...extraDetails, @@ -388,13 +386,30 @@ export const SecureSignup = ({ clearSubscriptionCache(); } + const trackingDetails: Record = {}; + + if (marketingOptInType) { + trackingDetails.marketingOptInType = marketingOptInType; + } + + if (!response.ok) { + trackingDetails.responseStatus = response.status; + const errorDescription = + await getResponseErrorDescription(response); + if (errorDescription) { + trackingDetails.errorDescription = errorDescription; + } + } + sendTracking( newsletterId, response.ok ? 'submission-confirmed' : 'submission-failed', renderingTarget, isSignedIn, abTest, - marketingOptInType ? { marketingOptInType } : undefined, + Object.keys(trackingDetails).length > 0 + ? trackingDetails + : undefined, ); }; @@ -443,6 +458,7 @@ export const SecureSignup = ({ renderingTarget, isSignedIn, abTest, + { errorType: getErrorType(error) }, ); setErrorMessage(`Sorry, there was an error signing you up.`); setIsWaitingForResponse(false); diff --git a/dotcom-rendering/src/components/SelfHostedVideo.island.tsx b/dotcom-rendering/src/components/SelfHostedVideo.island.tsx index 4cbd95f135b..615a6c94c7a 100644 --- a/dotcom-rendering/src/components/SelfHostedVideo.island.tsx +++ b/dotcom-rendering/src/components/SelfHostedVideo.island.tsx @@ -1,6 +1,7 @@ -import { css } from '@emotion/react'; +import { css, Global } from '@emotion/react'; import { isUndefined, log, storage } from '@guardian/libs'; import { from, space, until } from '@guardian/source/foundations'; +import type { RefObject } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { submitClickComponentEvent, @@ -10,12 +11,16 @@ import type { ArticleFormat } from '../lib/articleFormat'; import { getVideoClient } from '../lib/bridgetApi'; import { getZIndex } from '../lib/getZIndex'; import { generateImageURL } from '../lib/image'; +import { useAB } from '../lib/useAB'; +import { useFadeableControls } from '../lib/useFadeableControls'; import { hasMinimumBridgetVersion } from '../lib/useIsBridgetCompatible'; import { useIsInView } from '../lib/useIsInView'; +import { removeMediaRulePrefix, useMatchMedia } from '../lib/useMatchMedia'; import { useOnce } from '../lib/useOnce'; import { useShouldAdapt } from '../lib/useShouldAdapt'; import { useSubtitles } from '../lib/useSubtitles'; import { useVideoAttentionTracking } from '../lib/useVideoAttentionTracking'; +import { useVideoFullscreen } from '../lib/useVideoFullscreen'; import { useVideoMilestoneTracking } from '../lib/useVideoMilestoneTracking'; import type { CustomPlayEventDetail, Source } from '../lib/video'; import { @@ -50,12 +55,6 @@ import type { VideoEventKey } from './YoutubeAtom/YoutubeAtom'; */ const VISIBILITY_THRESHOLD = 0.5; -/** - * The duration in ms for which controls are displayed before fading out. - */ -const CONTROLS_FADE_DELAY = 3_000; -const PLAY_BUTTON_FADE_DELAY = 1_500; - const cardStyles = ( isInteractive: boolean, aspectRatioOfVisibleVideo: number, @@ -97,7 +96,7 @@ const maxHeightStyles = css` max-height: 80svh; } `; -const videoContainerStyles = ( +const videoViewportStyles = ( aspectRatio: number, aspectRatioOfVisibleVideo: number, greyBarsAtSidesOnDesktop: boolean, @@ -156,61 +155,6 @@ const videoContainerStyles = ( `} `; -const fullscreenStyles = css` - &:fullscreen { - display: flex; - align-items: center; - justify-content: center; - background-color: ${palette('--video-fullscreen-background')}; - width: 100vw; - height: 100vh; - - /* Override the fixed aspect-ratio + width:100% on the video so it - fits within the screen while preserving its aspect ratio. */ - - video { - width: 100%; - height: 100%; - max-width: 100vw; - max-height: 100vh; - aspect-ratio: auto; - object-fit: contain; - } - } -`; - -const showControlsStyles = css` - .controls-container { - visibility: visible; - opacity: 1; - } - - .play-pause-icon { - visibility: visible; - opacity: 1; - } -`; - -const hideControlsStyles = css` - .controls-container { - visibility: hidden; - opacity: 0; - transition: - visibility 500ms, - opacity 500ms ease-in-out; - transition-delay: ${CONTROLS_FADE_DELAY}ms; - } - - .play-pause-icon { - visibility: hidden; - opacity: 0; - transition: - visibility 400ms, - opacity 400ms ease-in-out; - transition-delay: ${PLAY_BUTTON_FADE_DELAY}ms; - } -`; - /** * Dispatches a custom play audio event so that other videos listening for this event will be muted. */ @@ -237,43 +181,38 @@ const logAndReportError = (src: string, error: Error) => { log('dotcom', message); }; +const MOBILE_VIDEO_WIDTH = 465; +/** + * 940 represents the widest possible video slot. + * This is a flexible general gigaboosted slot on desktop. + */ +const MAX_POSTER_WIDTH = 940; const getOptimisedPosterImage = ( mainImage: string, aspectRatio: string, + isTabletOrAbove: boolean, + measurementRef?: RefObject, ): string => { // This only runs on the client const resolution = window.devicePixelRatio >= 2 ? 'high' : 'low'; + /** + * Cards on mobile have variable widths. To avoid requesting many + * different image sizes, we use a single default width of 465px across + * all mobile devices. + */ + const imageWidth = !isTabletOrAbove + ? MOBILE_VIDEO_WIDTH + : (measurementRef?.current?.offsetWidth ?? MAX_POSTER_WIDTH); + return generateImageURL({ mainImage, - imageWidth: 940, // The widest a video can be: flexible special container, giga-boosted slot + imageWidth, resolution, aspectRatio, }); }; -/** - * The Fullscreen api is not supported by Safari mobile, - * so we need to check if we have access to the webkit api we can use instead. - * */ -const shouldUseWebkitFullscreen = (video: HTMLVideoElement): boolean => { - return ( - 'webkitDisplayingFullscreen' in video && - 'webkitEnterFullscreen' in video && - 'webkitExitFullscreen' in video - ); -}; - -/** - * The events we need to respond to for fullscreen tracking - * */ -const fullscreenChangeEvents = [ - 'fullscreenchange', - 'webkitfullscreenchange', - 'webkitbeginfullscreen', - 'webkitendfullscreen', -]; - /** * Ensures the aspect ratio falls between the minimum and maximum allowed aspect ratios, if specified. * For example, we may not want to render a square video inside a 4:5 feature card. In this case, the @@ -415,11 +354,30 @@ export const SelfHostedVideo = ({ }: Props) => { const adapted = useShouldAdapt(); const { renderingTarget } = useConfig(); - const vidRef = useRef(null); - const playerContainerRef = useRef(null); - const showControlsTimer = useRef(null); + const ab = useAB(); + const isInClickToPlayTest = + Boolean( + ab?.isUserInTestGroup( + 'fronts-and-curation-click-to-play', + 'variant', + ), + ) && videoStyle === 'Default'; + const videoStyleSettings: VideoStyleSettings = videoSettingsMap[videoStyle]; + + const willAttemptAutoplay = + videoStyleSettings.autoplay && !preventAutoplay && !isInClickToPlayTest; + const isTabletOrAbove = useMatchMedia(removeMediaRulePrefix(from.tablet)); + + const videoRef = useRef(null); + const videoContainerRef = useRef(null); + + const fullscreenContainerRef = useRef(null); const [isPlayable, setIsPlayable] = useState(false); - const [isMuted, setIsMuted] = useState(true); + /** + * Autoplay videos must start muted as browser autoplay policies require it. + * Click-to-play videos start unmuted as the user deliberately chose to play. + */ + const [isMuted, setIsMuted] = useState(willAttemptAutoplay); const [showPosterImage, setShowPosterImage] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(undefined); @@ -432,13 +390,7 @@ export const SelfHostedVideo = ({ const [width, setWidth] = useState(); const [height, setHeight] = useState(); const [optimisedSources, setOptimisedSources] = useState([]); - const [isFullscreen, setIsFullscreen] = useState(false); - const [isWebKitFullscreen, setIsWebKitFullscreen] = useState(false); const [isProgressBarSeeking, setIsProgressBarSeeking] = useState(false); - /** Whether the video should show controls */ - const [showControls, setShowControls] = useState(true); - /** Whether the video is currently showing controls */ - const [isShowingControls, setIsShowingControls] = useState(true); const isWeb = renderingTarget === 'Web'; const isApps = renderingTarget === 'Apps'; @@ -446,16 +398,18 @@ export const SelfHostedVideo = ({ const isLoopClickThroughTestVariant = videoStyle === 'Loop' && isInLoopClickTestVariant; - const videoStyleSettings: VideoStyleSettings = videoSettingsMap[videoStyle]; - /** * The video will autoplay if all of the following are true: * - the style of video allows autoplay * - the parent allows autoplay, i.e. we may not want to autoplay on certain page types * - autoplay is allowed by the browser, e.g. if "reduce motion" is enabled then we don't autoplay + * - the user is not in the click-to-play test variant */ const shouldAutoplay = - videoStyleSettings.autoplay && !preventAutoplay && isAutoplayAllowed; + videoStyleSettings.autoplay && + !preventAutoplay && + isAutoplayAllowed === true && + !isInClickToPlayTest; const showProgressBar = !hideProgressBar && @@ -486,11 +440,17 @@ export const SelfHostedVideo = ({ (playerState === 'PAUSED_BY_USER' || playerState === 'PAUSED_BY_BROWSER' || playerState === 'ENDED' || - (playerState === 'NOT_STARTED' && shouldAutoplay === false)); - - const showPauseIcon = - videoStyleSettings.hideControlsWhenNotInteractedWith && - playerState === 'PLAYING'; + (playerState === 'NOT_STARTED' && !shouldAutoplay)); + + const { + fadeableControlsStyles, + isShowingControls: isShowingFadeableControls, + showPauseIcon, + showFadeableControlsAndStartTimer, + } = useFadeableControls({ + playerState, + isEnabled: videoStyleSettings.enableFadeableControls, + }); let showPlayPauseIcon: 'play' | 'pause' | null = null; if (showPlayIcon) { @@ -520,20 +480,22 @@ export const SelfHostedVideo = ({ containerAspectRatioDesktop < aspectRatioOfVisibleVideo; const optimisedPosterImage = showPosterImage - ? getOptimisedPosterImage(posterImage, posterImageAspectRatio) + ? getOptimisedPosterImage( + posterImage, + posterImageAspectRatio, + isTabletOrAbove, + /** + * When the video is horizontally letterboxed (grey bars appear at the top and bottom), the + * height of the video is not known, so we measure + * the container. Otherwise, the video element matches the + * rendered width, so we measure the video directly. + */ + isGreyBarsAtTopAndBottomOnDesktop + ? videoContainerRef + : videoRef, + ) : undefined; - const showFadeableControls = - videoStyleSettings.hideControlsWhenNotInteractedWith && - !showControls && - playerState === 'PLAYING'; - - const hideFadeableControls = - videoStyleSettings.hideControlsWhenNotInteractedWith && - showControls && - (playerState === 'PAUSED_BY_USER' || - playerState === 'PAUSED_BY_BROWSER'); - const ophanVideoStyle = videoStyle.toLowerCase() as OphanVideoStyle; const sendOphanTrackingEvent = useCallback( @@ -564,7 +526,7 @@ export const SelfHostedVideo = ({ }); const activeCue = useSubtitles({ - video: vidRef.current, + video: videoRef.current, playerState, currentTime, }); @@ -582,7 +544,7 @@ export const SelfHostedVideo = ({ ); const playVideo = useCallback(async () => { - const video = vidRef.current; + const video = videoRef.current; if (!video) { return; } @@ -606,27 +568,30 @@ export const SelfHostedVideo = ({ } }, []); - const pauseVideo = ( - pauseReason: Extract< - PlayerStates, - | 'PAUSED_BY_USER' - | 'PAUSED_BY_INTERSECTION_OBSERVER' - | 'PAUSED_BY_BROWSER' - >, - ) => { - const video = vidRef.current; - if (!video) { - return; - } + const pauseVideo = useCallback( + ( + pauseReason: Extract< + PlayerStates, + | 'PAUSED_BY_USER' + | 'PAUSED_BY_INTERSECTION_OBSERVER' + | 'PAUSED_BY_BROWSER' + >, + ) => { + const video = videoRef.current; + if (!video) { + return; + } - if (pauseReason === 'PAUSED_BY_INTERSECTION_OBSERVER') { - setMutedState({ value: true, track: false }); - } + if (pauseReason === 'PAUSED_BY_INTERSECTION_OBSERVER') { + setMutedState({ value: true, track: false }); + } - setPlayerState(pauseReason); + setPlayerState(pauseReason); - void video.pause(); - }; + void video.pause(); + }, + [setMutedState], + ); const playPauseVideo = () => { if (playerState === 'PLAYING') { @@ -643,8 +608,8 @@ export const SelfHostedVideo = ({ }; const updateCurrentTime = (newTime: number) => { - if (vidRef.current) { - vidRef.current.currentTime = newTime; + if (videoRef.current) { + videoRef.current.currentTime = newTime; setCurrentTime(newTime); } }; @@ -778,6 +743,9 @@ export const SelfHostedVideo = ({ if (document.visibilityState === 'visible') { handlePageBecomesVisible(); } + if (renderingTarget === 'Apps' && document.hidden) { + pauseVideo('PAUSED_BY_BROWSER'); + } }); return () => { @@ -796,32 +764,13 @@ export const SelfHostedVideo = ({ handlePageBecomesVisible(); }); }; - }, [setMutedState, uniqueId, sources, renderingTarget]); - - /* Creates video-specific event listeners to handle fullscreen behaviour */ - useEffect(() => { - const video = vidRef.current; - if (!video) return; - - const handleEndFullscreen = () => { - setIsWebKitFullscreen(false); - positionCues(video); - }; - - video.addEventListener('webkitendfullscreen', handleEndFullscreen); - - return () => - video.removeEventListener( - 'webkitendfullscreen', - handleEndFullscreen, - ); - }, [positionCues]); + }, [setMutedState, uniqueId, sources, renderingTarget, pauseVideo]); /** * Track the first time the video comes into view. */ useOnce(() => { - const video = vidRef.current; + const video = videoRef.current; const resolution = video === null ? 'unknown' @@ -851,122 +800,34 @@ export const SelfHostedVideo = ({ */ useEffect(() => { if ( - shouldAutoplay === false || + !shouldAutoplay || (isInView === false && playerState === 'NOT_STARTED') ) { setShowPosterImage(true); } }, [shouldAutoplay, isInView, playerState]); - /** - * Capture fullscreen tracking events across browsers and devices - * We need to support events across: - * - Browsers with fullscreen API support - * - OSX Safari - * - iOS Safari - */ - useEffect(() => { - const video = vidRef.current; - const playerContainer = playerContainerRef.current; - - if (!playerContainer && !video) return; - - const updateStateAndReportFullscreenEvent = () => { - const isInFullscreenMode = - document.fullscreenElement !== null || - (video !== null && - 'webkitDisplayingFullscreen' in video && - Boolean(video.webkitDisplayingFullscreen)); - - if (isInFullscreenMode) { - setIsFullscreen(true); - } else { - setIsFullscreen(false); - } - - const event = isInFullscreenMode - ? 'enter_fullscreen' - : 'exit_fullscreen'; - - sendOphanTrackingEvent(event); - }; - - for (const event of fullscreenChangeEvents) { - if (video) { - video.addEventListener( - event, - updateStateAndReportFullscreenEvent, - ); - } - - if (playerContainer) { - playerContainer.addEventListener( - event, - updateStateAndReportFullscreenEvent, - ); - } - } - - return () => { - for (const event of fullscreenChangeEvents) { - if (video) { - video.removeEventListener( - event, - updateStateAndReportFullscreenEvent, - ); - } - - if (playerContainer) { - playerContainer.removeEventListener( - event, - updateStateAndReportFullscreenEvent, - ); - } - } - }; - }, [sendOphanTrackingEvent]); - - /** - * When the video starts playing, start a timer to hide the controls after a few seconds. - * If there is any user interaction while the video is playing, restart the timer. - * The controls will fade out after a period of no user interaction. - */ - useEffect(() => { - if (playerState !== 'PLAYING') { - return; - } - - /** - * We currently use this piece of state `showControls` as a self-resetting trigger. - * It's switched on to show the controls -> it's then immediately switched off to start - * the transition to hide the controls. - */ - setTimeout(() => { - setShowControls(false); - }, 0); - - if (showControlsTimer.current !== null) { - window.clearTimeout(showControlsTimer.current); - } - - showControlsTimer.current = window.setTimeout(() => { - setIsShowingControls(false); - }, CONTROLS_FADE_DELAY); - - return () => { - if (showControlsTimer.current !== null) { - window.clearTimeout(showControlsTimer.current); - showControlsTimer.current = null; - } - }; - }, [showControls, playerState]); + const { + isFullscreen, + isWebKitFullscreen, + handleFullscreenClick, + fullscreenStyles, + globalFullscreenStyles, + } = useVideoFullscreen({ + videoRef, + playerContainerRef: fullscreenContainerRef, + renderingTarget, + sendOphanTrackingEvent, + positionCues, + showFadeableControlsAndStartTimer, + }); if (adapted) { return FallbackImageComponent; } const handleLoadedMetadata = () => { - const video = vidRef.current; + const video = videoRef.current; if (!video) { return; } @@ -976,7 +837,7 @@ export const SelfHostedVideo = ({ }; const handleLoadedData = () => { - const video = vidRef.current; + const video = videoRef.current; if (!video) { return; } @@ -1000,13 +861,6 @@ export const SelfHostedVideo = ({ trackMilestones({ started: true }); }; - const showControlsAndStartTimer = () => { - if (!videoStyleSettings.hideControlsWhenNotInteractedWith) return; - - setShowControls(true); - setIsShowingControls(true); - }; - const handlePlayPauseClick = (event: React.SyntheticEvent) => { if (!videoStyleSettings.isInteractive) { return; @@ -1017,12 +871,8 @@ export const SelfHostedVideo = ({ * show the controls instead of pausing the video. * Note that hovering with a mouse shows controls on non-touch devices. */ - if ( - videoStyleSettings.hideControlsWhenNotInteractedWith && - playerState === 'PLAYING' && - !isShowingControls - ) { - showControlsAndStartTimer(); + if (playerState === 'PLAYING' && !isShowingFadeableControls) { + showFadeableControlsAndStartTimer(); return; } @@ -1039,7 +889,7 @@ export const SelfHostedVideo = ({ event.stopPropagation(); // Don't pause the video - showControlsAndStartTimer(); // Show controls when a button is clicked + showFadeableControlsAndStartTimer(); // Show controls when a button is clicked if (isMuted) { // Emit video play audio event so other components are aware when a video is played with sound @@ -1050,46 +900,6 @@ export const SelfHostedVideo = ({ } }; - const handleFullscreenClick = (event: React.SyntheticEvent) => { - void submitClickComponentEvent(event.currentTarget, renderingTarget); - event.stopPropagation(); // Don't pause the video - - showControlsAndStartTimer(); // Show controls when a button is clicked - - const video = vidRef.current; - if (!video) { - return; - } - - if (shouldUseWebkitFullscreen(video)) { - /*** - * webkit fullscreen methods are not part of the standard HTMLVideoElement - * type definition as they are iOS only. - * We need to extend the type expect these handlers when we're on iOS to keep TS happy. - * @see https://developer.apple.com/documentation/webkitjs/htmlvideoelement/1633500-webkitenterfullscreen - */ - const webkitVideo = video as HTMLVideoElement & { - webkitDisplayingFullscreen: boolean; - webkitEnterFullscreen: () => void; - webkitExitFullscreen: () => void; - }; - - if (webkitVideo.webkitDisplayingFullscreen) { - setIsWebKitFullscreen(false); - return webkitVideo.webkitExitFullscreen(); - } else { - setIsWebKitFullscreen(true); - return webkitVideo.webkitEnterFullscreen(); - } - } - - if (document.fullscreenElement) { - void document.exitFullscreen(); - } else if (playerContainerRef.current) { - void playerContainerRef.current.requestFullscreen(); - } - }; - /** * If the video was paused and we know that it wasn't paused by the user * or the intersection observer, we can deduce that it was paused by the @@ -1111,9 +921,11 @@ export const SelfHostedVideo = ({ }; const handleEnded = () => { - trackMilestones({ ended: true }); - resetMilestones(); - setPlayerState('ENDED'); + if (playerState === 'PLAYING') { + trackMilestones({ ended: true }); + resetMilestones(); + setPlayerState('ENDED'); + } }; /** @@ -1122,7 +934,7 @@ export const SelfHostedVideo = ({ */ const onError = () => { const message = `Self-hosted video could not be played. source: ${ - vidRef.current?.currentSrc ?? 'unknown' + videoRef.current?.currentSrc ?? 'unknown' }`; window.guardian.modules.sentry.reportError( @@ -1134,7 +946,7 @@ export const SelfHostedVideo = ({ }; const seekForward = () => { - const video = vidRef.current; + const video = videoRef.current; if (!video) { return; } @@ -1146,7 +958,7 @@ export const SelfHostedVideo = ({ }; const seekBackward = () => { - const video = vidRef.current; + const video = videoRef.current; if (!video) { return; } @@ -1158,7 +970,7 @@ export const SelfHostedVideo = ({ }; const handleTimeUpdate = () => { - const video = vidRef.current; + const video = videoRef.current; if (!video) { return; } @@ -1217,7 +1029,7 @@ export const SelfHostedVideo = ({ return; } - showControlsAndStartTimer(); + showFadeableControlsAndStartTimer(); const percentage = Number(event.currentTarget.value); const time = convertProgressPercentageToCurrentTime( @@ -1240,7 +1052,7 @@ export const SelfHostedVideo = ({ */ if (isPlayable) { if ( - shouldAutoplay === true && + shouldAutoplay && isInView === true && (playerState === 'NOT_STARTED' || playerState === 'PAUSED_BY_INTERSECTION_OBSERVER' || @@ -1250,13 +1062,18 @@ export const SelfHostedVideo = ({ setHasPageBecomeActive(false); } void playVideo(); - } else if (playerState === 'PLAYING' && isInView === false) { + } else if ( + playerState === 'PLAYING' && + isInView === false && + !isFullscreen + ) { void pauseVideo('PAUSED_BY_INTERSECTION_OBSERVER'); } } return (
    + {globalFullscreenStyles && ( + + )}
    { const posterImageUrl = element.posterImage?.[0]?.url; - const sources = extractValidSourcesFromAssets(element.assets, videoStyle); + const sources = extractValidSourcesFromAssets( + element.assets, + videoStyle, + element.duration, + ); const aspectRatio = getAspectRatioFromSources(sources); + const isVerticalVideo = aspectRatio < 1; const firstVideoSource = sources[0]; if (!posterImageUrl) { @@ -72,7 +77,9 @@ export const SelfHostedVideoInArticle = ({ isMainMedia={isMainMedia} role={role} preventAutoplay={videoStyle === 'Default'} - restrictHeightOnDesktop={!isInteractive(format.design)} + restrictHeightOnDesktop={ + isVerticalVideo && !isInteractive(format.design) + } />
    diff --git a/dotcom-rendering/src/components/SeriesSectionLink.tsx b/dotcom-rendering/src/components/SeriesSectionLink.tsx index fae4d263d26..803a78ba186 100644 --- a/dotcom-rendering/src/components/SeriesSectionLink.tsx +++ b/dotcom-rendering/src/components/SeriesSectionLink.tsx @@ -1,6 +1,5 @@ import { css } from '@emotion/react'; import { - between, from, headlineBold17, headlineBold20, @@ -83,23 +82,18 @@ const marginRight = css` } `; -const invertedStyle = (design: ArticleDesign) => { - if (design === ArticleDesign.Gallery) { - return ''; - } - return css` - /* Handle text wrapping onto a new line */ - white-space: pre-wrap; - box-decoration-break: clone; +const invertedStyle = css` + /* Handle text wrapping onto a new line */ + white-space: pre-wrap; + box-decoration-break: clone; + line-height: 28px; + padding-right: ${space[1]}px; + padding-top: ${space[1]}px; + padding-bottom: ${space[2]}px; + ${from.wide} { line-height: 28px; - ${from.leftCol} { - line-height: 28px; - } - padding-right: ${space[1]}px; - padding-top: ${space[1]}px; - padding-bottom: ${space[3]}px; - `; -}; + } +`; const fontStyles = (format: ArticleFormat) => { switch (format.design) { @@ -184,26 +178,15 @@ const breakWord = css` word-break: break-word; `; -const sectionPadding = (design: ArticleDesign) => { - if (design === ArticleDesign.Gallery) { - return css` - padding: 0 ${space[2]}px 0 ${space[3]}px; - - ${between.mobileLandscape.and.tablet} { - padding-left: ${space[5]}px; - } - `; +const sectionPadding = css` + padding-left: 10px; + ${from.mobileLandscape} { + padding-left: 18px; } - return css` - padding-left: 10px; - ${from.mobileLandscape} { - padding-left: 18px; - } - ${from.tablet} { - padding-left: ${space[1]}px; - } - `; -}; + ${from.tablet} { + padding-left: ${space[1]}px; + } +`; export const SeriesSectionLink = ({ format, @@ -358,9 +341,9 @@ export const SeriesSectionLink = ({ css={[ sectionLabelLink, fontStyles(format), - invertedStyle(format.design), + invertedStyle, breakWord, - sectionPadding(format.design), + sectionPadding, css` color: ${titleColour}; background-color: ${themePalette( @@ -369,7 +352,6 @@ export const SeriesSectionLink = ({ `, format.design === ArticleDesign.Gallery && css` - display: inline-block; position: relative; `, format.display === ArticleDisplay.Immersive && diff --git a/dotcom-rendering/src/components/SportDataPageComponent.tsx b/dotcom-rendering/src/components/SportDataPageComponent.tsx index 09631c5fc3a..3e9a68a046f 100644 --- a/dotcom-rendering/src/components/SportDataPageComponent.tsx +++ b/dotcom-rendering/src/components/SportDataPageComponent.tsx @@ -70,7 +70,6 @@ export const SportDataPageComponent = (props: Props) => { commercialMetricsEnabled={ !!sportData.config.switches.commercialMetrics } - tests={sportData.config.abTests} /> diff --git a/dotcom-rendering/src/components/Standfirst.tsx b/dotcom-rendering/src/components/Standfirst.tsx index 61be63fe1a3..0567d1dbc99 100644 --- a/dotcom-rendering/src/components/Standfirst.tsx +++ b/dotcom-rendering/src/components/Standfirst.tsx @@ -91,6 +91,10 @@ const decideFont = ({ display, design, theme }: ArticleFormat) => { return css` ${headlineMedium20} `; + case ArticleDesign.HostedGallery: + return css` + ${textSans24}; + `; case ArticleDesign.Obituary: case ArticleDesign.Comment: case ArticleDesign.Letter: @@ -322,6 +326,7 @@ const standfirstStyles = ({ display, design, theme }: ArticleFormat) => { color: ${palette('--standfirst-text')}; `; case ArticleDesign.Gallery: + case ArticleDesign.HostedGallery: return css` ${grid.span('centre-column-start', 5)} color: ${palette('--standfirst-text')}; diff --git a/dotcom-rendering/src/components/TagPage.tsx b/dotcom-rendering/src/components/TagPage.tsx index 3fbf6c27c5b..27326bb78db 100644 --- a/dotcom-rendering/src/components/TagPage.tsx +++ b/dotcom-rendering/src/components/TagPage.tsx @@ -69,7 +69,6 @@ export const TagPage = ({ tagPage, NAV }: Props) => { commercialMetricsEnabled={ !!tagPage.config.switches.commercialMetrics } - tests={tagPage.config.abTests} /> @@ -80,10 +79,7 @@ export const TagPage = ({ tagPage, NAV }: Props) => { - {isGoogleOneTapEnabled( - tagPage.config.abTests, - tagPage.config.switches, - ) && ( + {isGoogleOneTapEnabled(tagPage.config.switches) && ( diff --git a/dotcom-rendering/src/components/TimeDateline.tsx b/dotcom-rendering/src/components/TimeDateline.tsx index b3ec8fd2d9a..e21c46e35ab 100644 --- a/dotcom-rendering/src/components/TimeDateline.tsx +++ b/dotcom-rendering/src/components/TimeDateline.tsx @@ -4,6 +4,7 @@ import { ArticleDesign, type ArticleFormat } from '../lib/articleFormat'; import { palette } from '../palette'; const datelineStyles = css` + display: block; ${textSans12}; color: ${palette('--dateline')}; padding-top: 2px; @@ -14,30 +15,15 @@ const datelineStyles = css` } `; -const primaryStyles = css` - list-style: none; - cursor: pointer; - &::-webkit-details-marker { - display: none; - } -`; - -const hoverUnderline = css` - :hover { - text-decoration: underline; - } -`; - type Props = { primaryDateline: string; - secondaryDateline: string; webPublicationDate: string; format: ArticleFormat; }; +// This component is for trying to optimise the SEO date freshness by changing the primary date line to be a time element and removing any other dates from the page. export const TimeDateline = ({ primaryDateline, - secondaryDateline, webPublicationDate, format, }: Props) => { @@ -50,22 +36,6 @@ export const TimeDateline = ({ ? palette('--standfirst-text') : palette('--dateline'), }; - if (secondaryDateline && !secondaryDateline.includes(primaryDateline)) { - return ( -
    - - - - {secondaryDateline} -
    - ); - } return (
- {match.competitionName}, {match.venueName} + {match.stage}, {match.venueName}