diff --git a/dotcom-rendering/.storybook/preview.js b/dotcom-rendering/.storybook/preview.js index 8d78fa8b7bc..b61c0227880 100644 --- a/dotcom-rendering/.storybook/preview.js +++ b/dotcom-rendering/.storybook/preview.js @@ -23,6 +23,8 @@ if (isChromatic()) { MockDate.set('Sat Jan 1 2022 12:00:00 GMT+0000 (Greenwich Mean Time)'); } +window.IS_STORYBOOK = true; + mockRESTCalls(); setABTests( diff --git a/dotcom-rendering/cypress/e2e/parallel-1/article.e2e.cy.js b/dotcom-rendering/cypress/e2e/parallel-1/article.e2e.cy.js index 55251276f32..7af6f690811 100644 --- a/dotcom-rendering/cypress/e2e/parallel-1/article.e2e.cy.js +++ b/dotcom-rendering/cypress/e2e/parallel-1/article.e2e.cy.js @@ -78,7 +78,7 @@ describe('E2E Page rendering', function () { cy.get('gu-island[name=MostViewedFooterData]', { timeout: 30000 }) .scrollIntoView({ duration: 100 }) - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.get('[data-cy-ab-user-in-variant=ab-test-variant]').should( 'be.visible', @@ -104,7 +104,7 @@ describe('E2E Page rendering', function () { cy.get('gu-island[name=MostViewedFooterData]', { timeout: 30000 }) .scrollIntoView({ duration: 100 }) - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.get('[data-cy-ab-user-in-variant=ab-test-not-in-test]').should( 'be.visible', diff --git a/dotcom-rendering/cypress/e2e/parallel-3/article.interactivity.cy.js b/dotcom-rendering/cypress/e2e/parallel-3/article.interactivity.cy.js index 5957d5e5422..b04be510073 100644 --- a/dotcom-rendering/cypress/e2e/parallel-3/article.interactivity.cy.js +++ b/dotcom-rendering/cypress/e2e/parallel-3/article.interactivity.cy.js @@ -100,7 +100,7 @@ describe('Interactivity', function () { // Wait for hydration cy.get('gu-island[name=MostViewedFooterData]') .last() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.wait('@getMostRead'); cy.wait('@getMostReadGeo'); cy.get('[data-cy=mostviewed-footer]').should('exist'); diff --git a/dotcom-rendering/cypress/e2e/parallel-3/epic.interactivity.cy.js b/dotcom-rendering/cypress/e2e/parallel-3/epic.interactivity.cy.js index fb783367967..f72566894d0 100644 --- a/dotcom-rendering/cypress/e2e/parallel-3/epic.interactivity.cy.js +++ b/dotcom-rendering/cypress/e2e/parallel-3/epic.interactivity.cy.js @@ -32,7 +32,7 @@ describe('Epics', function () { // Wait for hydration of the Epic cy.get('gu-island[name=LiveBlogEpic]') .first() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.get('[data-cy=contributions-liveblog-epic]').scrollIntoView(); cy.get('[data-cy=contributions-liveblog-epic]').should('be.visible'); }); diff --git a/dotcom-rendering/cypress/e2e/parallel-5/liveblog.interactivity.cy.js b/dotcom-rendering/cypress/e2e/parallel-5/liveblog.interactivity.cy.js index 59fba1bc4d8..7d5509ebc64 100644 --- a/dotcom-rendering/cypress/e2e/parallel-5/liveblog.interactivity.cy.js +++ b/dotcom-rendering/cypress/e2e/parallel-5/liveblog.interactivity.cy.js @@ -47,7 +47,7 @@ describe('Liveblogs', function () { // Wait for hydration cy.get('gu-island[name=Liveness]') .first() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.scrollTo('center'); cy.get(`[data-cy="toast"]`).should('not.exist'); cy.window().then(function (win) { @@ -75,7 +75,7 @@ describe('Liveblogs', function () { // Wait for hydration cy.get('gu-island[name=Liveness]') .first() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.window().then(function (win) { win.mockLiveUpdate({ numNewBlocks: 1, @@ -92,7 +92,7 @@ describe('Liveblogs', function () { // Wait for hydration cy.get('gu-island[name=Liveness]', { timeout: 30000 }) .first() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.scrollTo('bottom'); cy.get(`[data-cy="toast"]`).should('not.exist'); cy.window().then(function (win) { @@ -128,7 +128,7 @@ describe('Liveblogs', function () { // Wait for hydration cy.get('gu-island[name=Liveness]') .first() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.window().then(function (win) { win.mockLiveUpdate({ numNewBlocks: 1, @@ -171,7 +171,7 @@ describe('Liveblogs', function () { // Wait for hydration cy.get('gu-island[name=Liveness]', { timeout: 30000 }) .first() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.scrollTo('bottom'); cy.get(`[data-cy="toast"]`).should('not.exist'); cy.window().then(function (win) { @@ -198,7 +198,7 @@ describe('Liveblogs', function () { // Wait for hydration cy.get('gu-island[name=Liveness]') .first() - .should('have.attr', 'data-island-status', 'rendered'); + .should('have.attr', 'data-island-status', 'hydrated'); cy.scrollTo('bottom', { duration: 1000 }); cy.window().then(function (win) { win.mockLiveUpdate({ diff --git a/dotcom-rendering/cypress/support/commands.js b/dotcom-rendering/cypress/support/commands.js index 30072395082..5ebd0bb4b6d 100644 --- a/dotcom-rendering/cypress/support/commands.js +++ b/dotcom-rendering/cypress/support/commands.js @@ -35,13 +35,10 @@ Cypress.Commands.add('hydrate', () => { const islandMeta = `island: ${name} defer: ${deferuntil}`; if (['idle', 'visible', undefined].includes(deferuntil)) { - const action = !!el.attr('clientOnly') - ? 'rendered' - : 'hydrated'; cy.log(`Scrolling to ${islandMeta}`); cy.wrap(el) .scrollIntoView({ duration: 1000, timeout: 30000 }) - .should('have.attr', 'data-island-status', action, { + .should('have.attr', 'data-island-status', 'hydrated', { timeout: 30000, }); // Additional wait to ensure island defer=visible has triggered diff --git a/dotcom-rendering/index.d.ts b/dotcom-rendering/index.d.ts index b10e2982ccd..9cfc739aa60 100644 --- a/dotcom-rendering/index.d.ts +++ b/dotcom-rendering/index.d.ts @@ -455,8 +455,6 @@ type RichLinkCardType = // 3rd party type declarations // // ------------------------------ -declare module 'chromatic/isChromatic'; - declare module 'dynamic-import-polyfill' { export const initialize: ({ modulePath, @@ -518,7 +516,6 @@ declare namespace JSX { name: string; deferUntil?: 'idle' | 'visible' | 'interaction' | 'hash'; rootMargin?: string; - clientOnly?: boolean; props: any; children: React.ReactNode; /** diff --git a/dotcom-rendering/src/client/islands/doHydration.tsx b/dotcom-rendering/src/client/islands/doHydration.tsx index a6d65442565..742447d1dc8 100644 --- a/dotcom-rendering/src/client/islands/doHydration.tsx +++ b/dotcom-rendering/src/client/islands/doHydration.tsx @@ -3,7 +3,7 @@ import type { EmotionCache } from '@emotion/react'; import { CacheProvider } from '@emotion/react'; import { log, startPerformanceMeasure } from '@guardian/libs'; import { createElement } from 'react'; -import { createRoot, hydrateRoot } from 'react-dom/client'; +import { hydrateRoot } from 'react-dom/client'; import { ConfigProvider } from '../../components/ConfigContext'; import type { Config } from '../../types/configContext'; @@ -13,7 +13,7 @@ declare global { * Defines the current state of the Island. * `undefined` at server-side rendering */ - islandStatus?: 'identified' | 'imported' | 'rendered' | 'hydrated'; + islandStatus?: 'identified' | 'imported' | 'hydrated'; } } @@ -21,9 +21,6 @@ declare global { * This function dynamically imports and then hydrates a specific component in * a specific part of the page * - * If the content being hydrated is not present in the dom then React renders - * it. This is how portals (non server side rendered content) work - * * @param name The name of the component we want to hydrate * @param data The deserialised props we want to use for hydration * @param element The location on the DOM where the component to hydrate exists @@ -52,47 +49,33 @@ export const doHydration = async ( .then((module) => { /** The duration of importing the module for this island */ const importDuration = endImportPerformanceMeasure(); - const clientOnly = element.hasAttribute('clientonly'); element.dataset.islandStatus = 'imported'; const { endPerformanceMeasure: endIslandPerformanceMeasure } = startPerformanceMeasure('dotcom', name, 'island'); - if (clientOnly) { - element.querySelector('[data-name="placeholder"]')?.remove(); - const root = createRoot(element); - root.render( - - - {createElement(module[name], data)} - - , - ); - } else { - hydrateRoot( - element, - - - {createElement(module[name], data)} - - , - ); - } + hydrateRoot( + element, + + + {createElement(module[name], data)} + + , + ); /** The duration of rendering or hydrating this island */ const islandDuration = endIslandPerformanceMeasure(); - return { clientOnly, importDuration, islandDuration }; + return { importDuration, islandDuration }; }) - .then(({ clientOnly, importDuration, islandDuration }) => { + .then(({ importDuration, islandDuration }) => { if (!('getEntriesByType' in window.performance)) return; - const action = clientOnly ? 'rendered' : 'hydrated'; - element.dataset.islandStatus = action; + element.dataset.islandStatus = 'hydrated'; log( 'dotcom', - `🏝 <${name} /> ${action} in ${islandDuration}ms (imported in ${importDuration}ms)`, + `🏝 hydrated <${name} /> in ${islandDuration}ms (imported in ${importDuration}ms)`, ); }) .catch((error) => { diff --git a/dotcom-rendering/src/client/islands/doStorybookHydration.js b/dotcom-rendering/src/client/islands/doStorybookHydration.js deleted file mode 100644 index f3431fed6ce..00000000000 --- a/dotcom-rendering/src/client/islands/doStorybookHydration.js +++ /dev/null @@ -1,55 +0,0 @@ -import { createElement } from 'react'; -import { createRoot } from 'react-dom/client'; -import { ConfigProvider } from '../../components/ConfigContext'; -import { getConfig } from './getConfig'; -import { getName } from './getName'; -import { getProps } from './getProps'; - -/** - * This is a cut down version of the islands/doHydration function that is - * used as part of the Islands flow for hydrating DCR pages. - * - * We don't use that specific function directly for two reasons: - * - * 1) We need to use React here, not Preact. Otherwise we get errors about - * conflicting versions of React - * - * 2) We don't want to defer hydration as this could affect our Chromatic - * snapshots - */ -export const doStorybookHydration = () => { - for (const element of document.querySelectorAll('gu-island')) { - if (element instanceof HTMLElement) { - const name = getName(element); - const props = getProps(element); - const config = getConfig(element); - - if (!name) continue; - if (element.getAttribute('deferuntil') === 'hash') continue; - - import( - /* webpackInclude: /\.importable\.tsx$/ */ - /* webpackChunkName: "[request]" */ - `../../components/${name}.importable` - ) - .then((module) => { - element - .querySelector('[data-name="placeholder"]') - ?.remove(); - const root = createRoot(element); - root.render( - - {createElement(module[name], props)} - , - ); - }) - .catch((e) => - // eslint-disable-next-line no-console -- We want to log here - console.error( - 'Failed hydration for storybook environment', - e, - ), - ); - } - } -}; diff --git a/dotcom-rendering/src/client/ophan/ophan.ts b/dotcom-rendering/src/client/ophan/ophan.ts index 32c088eacfc..35ab4accfe9 100644 --- a/dotcom-rendering/src/client/ophan/ophan.ts +++ b/dotcom-rendering/src/client/ophan/ophan.ts @@ -6,6 +6,7 @@ import type { OphanComponentEvent, } from '@guardian/libs'; import { log } from '@guardian/libs'; +import { isServer } from '../../lib/isServer'; import type { ServerSideTests } from '../../types/config'; export type OphanRecordFunction = ( @@ -55,15 +56,15 @@ export const getOphan = async (): Promise< * @deprecated use `getOphan` instead */ export const getOphanRecordFunction = (): OphanRecordFunction => { - const record = window.guardian.ophan?.record; - - if (record) return record; + if (isServer || window.guardian.ophan === undefined) { + // eslint-disable-next-line no-console -- worth informing all users + console.warn('window.guardian.ophan.record is not available'); + return () => { + /* do nothing */ + }; + } - // eslint-disable-next-line no-console -- worth informing all users - console.warn('window.guardian.ophan.record is not available'); - return () => { - /* do nothing */ - }; + return window.guardian.ophan.record; }; /** diff --git a/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx b/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx index ac0bea9d375..ce1bf957ec5 100644 --- a/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx +++ b/dotcom-rendering/src/components/AllEditorialNewslettersPage.tsx @@ -45,13 +45,13 @@ export const AllEditorialNewslettersPage = ({ /> - + - + - + { const isCompatible = useIsBridgetCompatible(); - if (!isCompatible) { + if (!isCompatible || isServer) { return null; } diff --git a/dotcom-rendering/src/components/ArticleBody.tsx b/dotcom-rendering/src/components/ArticleBody.tsx index 70cb740d756..011b974bf89 100644 --- a/dotcom-rendering/src/components/ArticleBody.tsx +++ b/dotcom-rendering/src/components/ArticleBody.tsx @@ -218,7 +218,7 @@ export const ArticleBody = ({ dir={languageDirection} > {isRecipe(tags) && ( - + )} diff --git a/dotcom-rendering/src/components/ArticleMeta.tsx b/dotcom-rendering/src/components/ArticleMeta.tsx index 989dbf7586e..d66c3f61f4d 100644 --- a/dotcom-rendering/src/components/ArticleMeta.tsx +++ b/dotcom-rendering/src/components/ArticleMeta.tsx @@ -449,7 +449,7 @@ export const ArticleMeta = ({ >
{isCommentable && ( - + { - + - + { )} - + {(format.design === ArticleDesign.LiveBlog || @@ -96,27 +96,27 @@ export const ArticlePage = (props: WebProps | AppProps) => { {renderingTarget === 'Web' && ( <> - + - + - + - + - + { )} {renderingTarget === 'Web' ? ( - + ) : ( - + { - useEffect(doStorybookHydration); - return (
{ - useOnce(doStorybookHydration, []); - return <>{children}; -}; - export default { component: DiscussionLayout, title: 'Components/DiscussionLayout', @@ -23,23 +16,21 @@ export const Basic = () => { storage.local.set('gu.prefs.discussion.order', 'newest'); return ( - - - + ); }; diff --git a/dotcom-rendering/src/components/DiscussionLayout.tsx b/dotcom-rendering/src/components/DiscussionLayout.tsx index 72d9f6da399..0882b1ac030 100644 --- a/dotcom-rendering/src/components/DiscussionLayout.tsx +++ b/dotcom-rendering/src/components/DiscussionLayout.tsx @@ -44,7 +44,7 @@ export const DiscussionLayout = ({ // If we're not hiding an advert stretch to the right stretchRight={!hideAd} leftContent={ - + + ) : ( diff --git a/dotcom-rendering/src/components/EnhancePinnedPost.importable.tsx b/dotcom-rendering/src/components/EnhancePinnedPost.importable.tsx index b1731bfade4..3d078da7fdb 100644 --- a/dotcom-rendering/src/components/EnhancePinnedPost.importable.tsx +++ b/dotcom-rendering/src/components/EnhancePinnedPost.importable.tsx @@ -81,6 +81,9 @@ const handleClickTracking = () => { } }; +/** + * Track how long the pinned post is in view. + */ export const EnhancePinnedPost = () => { const [hasBeenSeen, setHasBeenSeen] = useState(false); const [isInView] = useIsInView({ @@ -158,5 +161,6 @@ export const EnhancePinnedPost = () => { } } }, [isInView, hasBeenSeen]); + return null; }; diff --git a/dotcom-rendering/src/components/EuropeLandingModal.importable.tsx b/dotcom-rendering/src/components/EuropeLandingModal.importable.tsx index 02be2c3c0aa..7c393024744 100644 --- a/dotcom-rendering/src/components/EuropeLandingModal.importable.tsx +++ b/dotcom-rendering/src/components/EuropeLandingModal.importable.tsx @@ -18,6 +18,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { EditionId } from '../lib/edition'; import { getEditionFromId } from '../lib/edition'; import { guard } from '../lib/guard'; +import { isServer } from '../lib/isServer'; import { nestedOphanComponents } from '../lib/ophan-helpers'; import { SvgFlagsInCircle } from './SvgFlagsInCircle'; @@ -196,7 +197,7 @@ export const getModalType = (): ModalType => { interface Props { edition: EditionId; } -export const EuropeLandingModal = ({ edition }: Props) => { +const ClientOnlyEuropeLandingModal = ({ edition }: Props) => { const editionCookie = getCookie({ name: 'GU_EDITION' }); const modalType = getModalType(); const [switchEdition, setSwitchEdition] = useState(false); @@ -418,3 +419,13 @@ export const EuropeLandingModal = ({ edition }: Props) => { ); }; + +/** + * This is a weird way of handling this client-only island, + * but as this component is temporary, it was deemed + * a more pragmatic approach over having to rewrite it entirely. + * + * (October 2023) + */ +export const EuropeLandingModal = (props: Props) => + isServer ? null : ; diff --git a/dotcom-rendering/src/components/Footer.tsx b/dotcom-rendering/src/components/Footer.tsx index b017c3feb67..ccefdfdd29f 100644 --- a/dotcom-rendering/src/components/Footer.tsx +++ b/dotcom-rendering/src/components/Footer.tsx @@ -271,7 +271,7 @@ const FooterLinks = ({ const rrLinks = (
- + { /> - + - + - + - + - + - + - + - + diff --git a/dotcom-rendering/src/components/Header.tsx b/dotcom-rendering/src/components/Header.tsx index 043f04c4da2..2296db12e3f 100644 --- a/dotcom-rendering/src/components/Header.tsx +++ b/dotcom-rendering/src/components/Header.tsx @@ -73,11 +73,11 @@ export const Header = ({
- + - + { + if (isServer) return null; + const hideSupportMessaging = shouldHideSupportMessaging(); if (!hideSupportMessaging) { diff --git a/dotcom-rendering/src/components/Island.tsx b/dotcom-rendering/src/components/Island.tsx index 54507b53c74..2554f61ce5a 100644 --- a/dotcom-rendering/src/components/Island.tsx +++ b/dotcom-rendering/src/components/Island.tsx @@ -1,22 +1,16 @@ import { useConfig } from './ConfigContext'; -/** - * An island can be server-side rendered and then hydrated on the client, - * or simply rendered on the client (with the server rendering nothing). - */ -type ClientOnlyProps = { clientOnly?: true }; - -type DefaultProps = ClientOnlyProps & { +type DefaultProps = { deferUntil?: never; rootMargin?: never; children: JSX.Element; }; /** - * The possible props for an island that should be hydrated/rendered when it + * The possible props for an island that should be hydrated when it * becomes visible */ -type VisibleProps = ClientOnlyProps & { +type VisibleProps = { deferUntil: 'visible'; /** * @see https://developer.mozilla.org/en-us/docs/web/api/intersectionobserver/rootmargin @@ -26,10 +20,10 @@ type VisibleProps = ClientOnlyProps & { }; /** - * The possible props for an island that should be hydrated/rendered when the + * The possible props for an island that should be hydrated when the * browser is idle */ -type IdleProps = ClientOnlyProps & { +type IdleProps = { deferUntil: 'idle'; rootMargin?: never; children: JSX.Element; @@ -41,18 +35,16 @@ type IdleProps = ClientOnlyProps & { */ type InteractionProps = { deferUntil: 'interaction'; - clientOnly?: never; rootMargin?: never; children: JSX.Element; }; /** - * The possible props for an island that should be rendered when a user adds a + * The possible props for an island that should be hydrated when a user adds a * hash fragment to the page URL */ type HashProps = { deferUntil: 'hash'; - clientOnly: true; rootMargin?: never; children: JSX.Element; }; @@ -83,15 +75,9 @@ type Props = * - idle - Execute when browser idle * - visible - Execute when component appears in viewport * - interaction - Execute when component is clicked on in the viewport - * @param {boolean} props.clientOnly - Should the component be server side rendered * @param {JSX.Element} props.children - The component being inserted. Must be a single JSX Element */ -export const Island = ({ - deferUntil, - clientOnly, - rootMargin, - children, -}: Props) => { +export const Island = ({ deferUntil, rootMargin, children }: Props) => { /** * Where is this coming from? * Config value is set at high in the component tree within a React context in a `` @@ -106,11 +92,10 @@ export const Island = ({ name={children.type.name} deferUntil={deferUntil} props={JSON.stringify(children.props)} - clientOnly={clientOnly} rootMargin={rootMargin} config={JSON.stringify(config)} > - {clientOnly ? null : children} + {children} ); }; @@ -120,6 +105,6 @@ export const Island = ({ */ export const islandNoscriptStyles = ` `; diff --git a/dotcom-rendering/src/components/LatestLinks.importable.stories.tsx b/dotcom-rendering/src/components/LatestLinks.importable.stories.tsx index 3fc52381721..3df79d70487 100644 --- a/dotcom-rendering/src/components/LatestLinks.importable.stories.tsx +++ b/dotcom-rendering/src/components/LatestLinks.importable.stories.tsx @@ -4,8 +4,6 @@ import { ArticleDesign, ArticleDisplay, Pillar } from '@guardian/libs'; import { breakpoints, palette } from '@guardian/source-foundations'; import fetchMock from 'fetch-mock'; import type { PropsWithChildren } from 'react'; -import { useEffect } from 'react'; -import { doStorybookHydration } from '../client/islands/doStorybookHydration'; import { decideContainerOverrides } from '../lib/decideContainerOverrides'; import type { DCRContainerPalette } from '../types/front'; import { Island } from './Island'; @@ -30,8 +28,6 @@ const Wrapper = ({ children, styles, }: PropsWithChildren<{ styles: SerializedStyles }>) => { - useEffect(doStorybookHydration); - fetchMock .restore() .get( diff --git a/dotcom-rendering/src/components/LightboxJavascript.importable.tsx b/dotcom-rendering/src/components/LightboxJavascript.importable.tsx index bc3cb672e99..654557423ea 100644 --- a/dotcom-rendering/src/components/LightboxJavascript.importable.tsx +++ b/dotcom-rendering/src/components/LightboxJavascript.importable.tsx @@ -480,7 +480,7 @@ function initialiseLightbox(lightbox: HTMLElement) { } // Mark the lightbox as ready so that we don't try to re-initialise it later - lightbox.setAttribute('data-island-status', 'rendered'); + lightbox.setAttribute('data-island-status', 'hydrated'); } export const LightboxJavascript = ({ diff --git a/dotcom-rendering/src/components/LiveBlogEpic.importable.tsx b/dotcom-rendering/src/components/LiveBlogEpic.importable.tsx index 6970b743ccb..85d199cef1c 100644 --- a/dotcom-rendering/src/components/LiveBlogEpic.importable.tsx +++ b/dotcom-rendering/src/components/LiveBlogEpic.importable.tsx @@ -15,6 +15,7 @@ import { useHasOptedOutOfArticleCount, } from '../lib/contributions'; import { getLocaleCode } from '../lib/getCountryCode'; +import { isServer } from '../lib/isServer'; import { setAutomat } from '../lib/setAutomat'; import { useAuthStatus } from '../lib/useAuthStatus'; import { useSDCLiveblogEpic } from '../lib/useSDC'; @@ -103,9 +104,11 @@ const usePayload = ({ const articleCounts = useArticleCounts(pageId, keywordIds); const hasOptedOutOfArticleCount = useHasOptedOutOfArticleCount(); const countryCode = useCountryCode(); + const authStatus = useAuthStatus(); + if (isServer) return; + const mvtId = Number(getCookie({ name: 'GU_mvt_id', shouldMemoize: true })) || 0; - const authStatus = useAuthStatus(); const isSignedIn = authStatus.kind === 'SignedInWithOkta' || authStatus.kind === 'SignedInWithCookies'; diff --git a/dotcom-rendering/src/components/Liveness.importable.tsx b/dotcom-rendering/src/components/Liveness.importable.tsx index e2cdb0252ea..7dc3dae31f9 100644 --- a/dotcom-rendering/src/components/Liveness.importable.tsx +++ b/dotcom-rendering/src/components/Liveness.importable.tsx @@ -5,6 +5,7 @@ import { initHydration } from '../client/islands/initHydration'; import { updateTimeElement } from '../client/relativeTime/updateTimeElements'; import { isServer } from '../lib/isServer'; import { useApi } from '../lib/useApi'; +import { useHydrated } from '../lib/useHydrated'; import { Toast } from './Toast'; type Props = { @@ -167,6 +168,7 @@ export const Liveness = ({ const [topOfBlogVisible, setTopOfBlogVisible] = useState(); const [numHiddenBlocks, setNumHiddenBlocks] = useState(0); const [latestBlockId, setLatestBlockId] = useState(mostRecentBlockId); + const hydrated = useHydrated(); /** * This function runs (once) after every successful useApi call. This is useful because it @@ -220,7 +222,7 @@ export const Liveness = ({ * updates with whatever html and properties it wants * */ - window.mockLiveUpdate = onSuccess; + !isServer && (window.mockLiveUpdate = onSuccess); // useApi returns { data, loading, error } but we're not using them here useApi( @@ -312,7 +314,7 @@ export const Liveness = ({ } }, [hasPinnedPost, onFirstPage, webURL]); - if (toastRoot && showToast) { + if (toastRoot && showToast && hydrated) { /** * Why `createPortal`? * diff --git a/dotcom-rendering/src/components/Metrics.importable.tsx b/dotcom-rendering/src/components/Metrics.importable.tsx index 81bab4c1f1b..25ae864940a 100644 --- a/dotcom-rendering/src/components/Metrics.importable.tsx +++ b/dotcom-rendering/src/components/Metrics.importable.tsx @@ -10,9 +10,11 @@ import { import { getCookie } from '@guardian/libs'; import { billboardsInMerchHigh } from '../experiments/tests/billboards-in-merch-high'; import { integrateIma } from '../experiments/tests/integrate-ima'; +import { isServer } from '../lib/isServer'; import { useAB } from '../lib/useAB'; import { useAdBlockInUse } from '../lib/useAdBlockInUse'; import { useOnce } from '../lib/useOnce'; +import { usePageViewId } from '../lib/usePageViewId'; type Props = { commercialMetricsEnabled: boolean; @@ -29,21 +31,31 @@ const clientSideTestsToForceMetrics: ABTest[] = [ billboardsInMerchHigh, ]; +/** + * Send web vitals and commercial metrics to our data warehouse + * + * ## Why does this need to be an Island + * + * It cannot run on the server and needs to send data about the current page view + */ export const Metrics = ({ commercialMetricsEnabled }: Props) => { const abTestApi = useAB()?.api; const adBlockerInUse = useAdBlockInUse(); - const browserId = getCookie({ name: 'bwid', shouldMemoize: true }); - const { pageViewId } = window.guardian.config.ophan; + const browserId = isServer + ? null + : getCookie({ name: 'bwid', shouldMemoize: true }); + const pageViewId = usePageViewId(); - const isDev = - !!window.guardian.config.page.isDev || - window.location.hostname === 'm.code.dev-theguardian.com' || - window.location.hostname === (process.env.HOSTNAME ?? 'localhost') || - window.location.hostname === 'preview.gutools.co.uk'; + const isDev = isServer + ? undefined + : !!window.guardian.config.page.isDev || + window.location.hostname === 'm.code.dev-theguardian.com' || + window.location.hostname === (process.env.HOSTNAME ?? 'localhost') || + window.location.hostname === 'preview.gutools.co.uk'; const userInServerSideTest = - Object.keys(window.guardian.config.tests).length > 0; + !isServer && Object.keys(window.guardian.config.tests).length > 0; const shouldBypassSampling = (api: ABTestAPI) => willRecordCoreWebVitals || @@ -63,6 +75,8 @@ export const Metrics = ({ commercialMetricsEnabled }: Props) => { */ const nearZeroSampling = Number.MIN_VALUE; + if (isDev === undefined) return; + void initCoreWebVitals({ browserId, pageViewId, @@ -74,7 +88,7 @@ export const Metrics = ({ commercialMetricsEnabled }: Props) => { if (bypassSampling || isDev) void bypassCoreWebVitalsSampling('commercial'); }, - [abTestApi], + [abTestApi, isDev], ); useOnce( @@ -87,6 +101,8 @@ export const Metrics = ({ commercialMetricsEnabled }: Props) => { ? shouldBypassSampling(abTestApi) : false; + if (!pageViewId || isDev === undefined) return; + initCommercialMetrics({ pageViewId, browserId: browserId ?? undefined, @@ -104,7 +120,7 @@ export const Metrics = ({ commercialMetricsEnabled }: Props) => { ), ); }, - [abTestApi, adBlockerInUse, commercialMetricsEnabled], + [abTestApi, adBlockerInUse, commercialMetricsEnabled, isDev], ); // We don’t render anything diff --git a/dotcom-rendering/src/components/MostViewedFooterData.importable.tsx b/dotcom-rendering/src/components/MostViewedFooterData.importable.tsx index 797f3183acb..56ffd81ba15 100644 --- a/dotcom-rendering/src/components/MostViewedFooterData.importable.tsx +++ b/dotcom-rendering/src/components/MostViewedFooterData.importable.tsx @@ -67,10 +67,12 @@ export const MostViewedFooterData = ({ const variantFromRunnable = runnableTest?.variantToRun.id ?? 'not-runnable'; const url = buildSectionUrl(ajaxUrl, edition, sectionId); - const { data, error } = useApi< + const { data, error, loading } = useApi< MostViewedFooterPayloadType | FETrailTabType[] >(url); + if (loading) return null; + if (error) { window.guardian.modules.sentry.reportError(error, 'most-viewed-footer'); return null; diff --git a/dotcom-rendering/src/components/MostViewedFooterLayout.stories.tsx b/dotcom-rendering/src/components/MostViewedFooterLayout.stories.tsx index cfc742029a2..0ec24762168 100644 --- a/dotcom-rendering/src/components/MostViewedFooterLayout.stories.tsx +++ b/dotcom-rendering/src/components/MostViewedFooterLayout.stories.tsx @@ -1,17 +1,8 @@ -import React, { useEffect } from 'react'; import { trails } from '../../fixtures/manual/trails'; -import { doStorybookHydration } from '../client/islands/doStorybookHydration'; import { MostViewedFooter } from './MostViewedFooter.importable'; import { MostViewedFooterLayout } from './MostViewedFooterLayout'; import { Section } from './Section'; -const Hydrated = ({ children }: { children: React.ReactNode }) => { - useEffect(() => { - doStorybookHydration(); - }); - return <>{children}; -}; - export default { component: MostViewedFooterLayout, title: 'Components/MostViewedFooter', @@ -22,76 +13,68 @@ export default { export const withTwoTabsAdFree = () => { return ( - -
- - - -
-
+
+ + + +
); }; withTwoTabsAdFree.storyName = 'with two tabs ad free'; export const withOneTabsAdFree = () => { return ( - -
- - - -
-
+
+ + + +
); }; withOneTabsAdFree.storyName = 'with one tab ad free'; export const withTwoTabs = () => { return ( - -
- - - -
-
+
+ + + +
); }; withTwoTabs.storyName = 'with two tabs'; export const withOneTabs = () => { return ( - -
- - - -
-
+
+ + + +
); }; withOneTabs.storyName = 'with one tab'; diff --git a/dotcom-rendering/src/components/MostViewedRightWithAd.tsx b/dotcom-rendering/src/components/MostViewedRightWithAd.tsx index 6259642b4f4..b122401a0c0 100644 --- a/dotcom-rendering/src/components/MostViewedRightWithAd.tsx +++ b/dotcom-rendering/src/components/MostViewedRightWithAd.tsx @@ -44,7 +44,6 @@ export const MostViewedRightWithAd = ({ {!isPaidContent ? ( {isImmersive && ( - + { - const [countryCode, setCountryCode] = useState(); - const pageViewId = window.guardian.config.ophan.pageViewId; + const countryCode = useCountryCode(); + const pageViewId = usePageViewId(); const ophanRecord = getOphanRecordFunction(); - useEffect(() => { - const callFetch = () => { - getLocaleCode() - .then((cc) => { - setCountryCode(cc ?? ''); - }) - .catch((e) => - console.error(`countryCodePromise - error: ${String(e)}`), - ); - }; - callFetch(); - }, []); + if (!countryCode || !pageViewId) return null; - if (countryCode) { - if (inHeader && remoteHeader) { - return ( - - ); - } + if (inHeader && remoteHeader) { return ( - ); } - - return null; + return ( + + ); }; diff --git a/dotcom-rendering/src/components/RecipeMultiplier.importable.tsx b/dotcom-rendering/src/components/RecipeMultiplier.importable.tsx index e47aa6499c7..01e1d9c3676 100644 --- a/dotcom-rendering/src/components/RecipeMultiplier.importable.tsx +++ b/dotcom-rendering/src/components/RecipeMultiplier.importable.tsx @@ -248,6 +248,8 @@ export const RecipeMultiplier = () => { } }, [multiplier]); + if (isServer) return null; + const handleChange: ChangeEventHandler = ({ target: { value }, }) => { diff --git a/dotcom-rendering/src/components/SetABTests.importable.tsx b/dotcom-rendering/src/components/SetABTests.importable.tsx index b482e837755..405f90896a9 100644 --- a/dotcom-rendering/src/components/SetABTests.importable.tsx +++ b/dotcom-rendering/src/components/SetABTests.importable.tsx @@ -7,6 +7,7 @@ import { getCypressSwitches } from '../experiments/cypress-switches'; import { runnableTestsToParticipations } from '../experiments/lib/ab-participations'; import { getForcedParticipationsFromUrl } from '../lib/getAbUrlHash'; import { setABTests } from '../lib/useAB'; +import { useHydrated } from '../lib/useHydrated'; import type { ABTestSwitches } from '../model/enhance-switches'; type Props = { @@ -33,6 +34,9 @@ export const SetABTests = ({ abTestSwitches, forcedTestVariants, }: Props) => { + const hydrated = useHydrated(); + if (!hydrated) return null; + const mvtId = Number( (isDev && getCookie({ name: 'GU_mvt_id_local', shouldMemoize: true })) || // Simplify localhost testing by creating a different mvt id diff --git a/dotcom-rendering/src/components/SetAdTargeting.importable.tsx b/dotcom-rendering/src/components/SetAdTargeting.importable.tsx index c10330fd651..89b388e2aa1 100644 --- a/dotcom-rendering/src/components/SetAdTargeting.importable.tsx +++ b/dotcom-rendering/src/components/SetAdTargeting.importable.tsx @@ -1,15 +1,14 @@ import { log } from '@guardian/libs'; -import { isServer } from '../lib/isServer'; import { setAdTargeting } from '../lib/useAdTargeting'; +import { useHydrated } from '../lib/useHydrated'; type Props = { adTargeting: AdTargeting; }; export const SetAdTargeting = ({ adTargeting }: Props) => { - if (isServer) { - throw new Error('SetAdTargeting is client only'); - } + const hydrated = useHydrated(); + if (!hydrated) return null; setAdTargeting(adTargeting); log('commercial', '🎯 Ad targeting', adTargeting); diff --git a/dotcom-rendering/src/components/SignInGate/README.md b/dotcom-rendering/src/components/SignInGate/README.md index 270fd273e7f..b4390b36a87 100644 --- a/dotcom-rendering/src/components/SignInGate/README.md +++ b/dotcom-rendering/src/components/SignInGate/README.md @@ -288,7 +288,7 @@ The disadvantage of this method is that it's a bit tricky to work out exactly wh **B)** Add the `forcedTestVariant` prop to the `SetABTests` Island: ```tsx - + ; }; -// component with conditional logic which determines if a sign in gate -// should be shown on the current page +/** + * Component with conditional logic which determines if a sign in gate + * should be shown on the current page + * + * ## Why does this need to be an Island? + * + * The decision is specific to a page view. + */ export const SignInGateSelector = ({ contentType, sectionId = '', @@ -172,14 +180,16 @@ export const SignInGateSelector = ({ const [canShowGate, setCanShowGate] = useState(false); const gateSelector = useSignInGateSelector(); - const { pageViewId } = window.guardian.config.ophan; + const pageViewId = usePageViewId(); // START: Checkout Complete Personalisation const [personaliseSwitch, setPersonaliseSwitch] = useState(false); - const checkOutCompleteString = getCookie({ - name: 'GU_CO_COMPLETE', - shouldMemoize: true, - }); + const checkOutCompleteString = isServer + ? null + : getCookie({ + name: 'GU_CO_COMPLETE', + shouldMemoize: true, + }); const checkoutCompleteCookieData: CheckoutCompleteCookieData | undefined = checkOutCompleteString !== null ? parseCheckoutCompleteCookieData(checkOutCompleteString) @@ -260,6 +270,8 @@ export const SignInGateSelector = ({ ? personaliseComponentId(signInGateComponentId) : signInGateComponentId; + if (!pageViewId) return null; + const ctaUrlParams = { pageId, host, diff --git a/dotcom-rendering/src/components/SlotBodyEnd.importable.tsx b/dotcom-rendering/src/components/SlotBodyEnd.importable.tsx index 3b838d620c1..7af25552da1 100644 --- a/dotcom-rendering/src/components/SlotBodyEnd.importable.tsx +++ b/dotcom-rendering/src/components/SlotBodyEnd.importable.tsx @@ -6,7 +6,7 @@ import { getCookie } from '@guardian/libs'; import type { WeeklyArticleHistory } from '@guardian/support-dotcom-components/dist/dotcom/src/types'; import { useEffect, useState } from 'react'; import { getArticleCounts } from '../lib/articleCount'; -import { getLocaleCode } from '../lib/getCountryCode'; +import { isServer } from '../lib/isServer'; import type { CandidateConfig, MaybeFC, @@ -15,6 +15,7 @@ import type { import { pickMessage } from '../lib/messagePicker'; import { type AuthStatus, useAuthStatus } from '../lib/useAuthStatus'; import { useBraze } from '../lib/useBraze'; +import { useCountryCode } from '../lib/useCountryCode'; import { useOnce } from '../lib/useOnce'; import type { TagType } from '../types/tag'; import { AdSlot } from './AdSlot.web'; @@ -105,6 +106,13 @@ function getIsSignedIn(authStatus: AuthStatus): boolean | undefined { } } +/** + * Conditionally show an “epic” at the end of a article. + * + * ## Why does this need to be an Island? + * + * The decision is specific to a page view. + */ export const SlotBodyEnd = ({ contentType, sectionId, @@ -121,9 +129,11 @@ export const SlotBodyEnd = ({ isLabs, }: Props) => { const { brazeMessages } = useBraze(idApiUrl); - const [countryCode, setCountryCode] = useState(); + const countryCode = useCountryCode(); const isSignedIn = getIsSignedIn(useAuthStatus()); - const browserId = getCookie({ name: 'bwid', shouldMemoize: true }); + const browserId = isServer + ? null + : getCookie({ name: 'bwid', shouldMemoize: true }); const [SelectedEpic, setSelectedEpic] = useState( null, ); @@ -132,24 +142,12 @@ export const SlotBodyEnd = ({ // Show the article end slot if the epic is not shown, currently only used in the US for Public Good const showArticleEndSlot = + !isServer && renderAds && !isLabs && countryCode === 'US' && window.guardian.config.switches.articleEndSlot; - useEffect(() => { - const callFetch = () => { - getLocaleCode() - .then((cc) => { - setCountryCode(cc ?? ''); - }) - .catch((e) => - console.error(`countryCodePromise - error: ${String(e)}`), - ); - }; - callFetch(); - }, []); - useEffect(() => { setAsyncArticleCount( getArticleCounts(pageId, keywordIds).then( diff --git a/dotcom-rendering/src/components/Snow.importable.tsx b/dotcom-rendering/src/components/Snow.importable.tsx index 1a73a5865d3..d84aef5e80f 100644 --- a/dotcom-rendering/src/components/Snow.importable.tsx +++ b/dotcom-rendering/src/components/Snow.importable.tsx @@ -2,6 +2,7 @@ import { css } from '@emotion/react'; import { body, neutral } from '@guardian/source-foundations'; import { Link, SvgChevronDownSingle } from '@guardian/source-react-components'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useHydrated } from '../lib/useHydrated'; type Flake = { x: number; @@ -69,9 +70,15 @@ const generateFlake = () => { ); }; +/** + * A winter version of an easter egg… + */ export const Snow = () => { + const hydrated = useHydrated(false); + const [flakes, setFlakes] = useState(() => { const initial: Snowflakes = {}; + if (!hydrated) return initial; for (let i = 0; i < 10; i++) { const generatedFlake = generateFlake(); initial[generatedFlake.name] = generatedFlake; @@ -163,6 +170,8 @@ export const Snow = () => { )); }, [flakes]); + if (!hydrated) return null; + return (
{ const { brazeMessages } = useBraze(idApiUrl); - const asyncCountryCode = getLocaleCode(); + const asyncCountryCode = isServer ? Promise.resolve(null) : getLocaleCode(); const authStatus = useAuthStatus(); const isSignedIn = authStatus.kind === 'SignedInWithOkta' || diff --git a/dotcom-rendering/src/components/SupportTheG.importable.tsx b/dotcom-rendering/src/components/SupportTheG.importable.tsx index 7580f347863..1d746252fb1 100644 --- a/dotcom-rendering/src/components/SupportTheG.importable.tsx +++ b/dotcom-rendering/src/components/SupportTheG.importable.tsx @@ -32,13 +32,14 @@ import { shouldHideSupportMessaging, } from '../lib/contributions'; import type { EditionId } from '../lib/edition'; -import { getLocaleCode } from '../lib/getCountryCode'; import { nestedOphanComponents } from '../lib/ophan-helpers'; import { setAutomat } from '../lib/setAutomat'; import type { AuthStatus } from '../lib/useAuthStatus'; import { useAuthStatus } from '../lib/useAuthStatus'; +import { useCountryCode } from '../lib/useCountryCode'; import { useIsInView } from '../lib/useIsInView'; import { useOnce } from '../lib/useOnce'; +import { usePageViewId } from '../lib/usePageViewId'; import ArrowRightIcon from '../static/icons/arrow-right.svg'; type Props = { @@ -431,46 +432,31 @@ export const SupportTheG = ({ contributionsServiceUrl, hasPageSkin = false, }: Props) => { - const [countryCode, setCountryCode] = useState(); - const pageViewId = window.guardian.config.ophan.pageViewId; + const countryCode = useCountryCode(); + const pageViewId = usePageViewId(); const ophanRecord = getOphanRecordFunction(); - useEffect(() => { - const callFetch = () => { - getLocaleCode() - .then((cc) => { - setCountryCode(cc ?? ''); - }) - .catch((e) => - console.error(`countryCodePromise - error: ${String(e)}`), - ); - }; - callFetch(); - }, []); + if (!countryCode || !pageViewId) return null; - if (countryCode) { - if (inHeader && remoteHeader) { - return ( - - ); - } + if (inHeader && remoteHeader) { return ( - ); } - - return null; + return ( + + ); }; diff --git a/dotcom-rendering/src/components/TagFrontPage.tsx b/dotcom-rendering/src/components/TagFrontPage.tsx index 3368caa2936..5a54076c1a7 100644 --- a/dotcom-rendering/src/components/TagFrontPage.tsx +++ b/dotcom-rendering/src/components/TagFrontPage.tsx @@ -54,20 +54,20 @@ export const TagFrontPage = ({ tagFront, NAV }: Props) => { /> - + - + - + - + { isDev={!!tagFront.config.isDev} /> - + diff --git a/dotcom-rendering/src/layouts/CommentLayout.tsx b/dotcom-rendering/src/layouts/CommentLayout.tsx index 28236fe90b0..d32bd46ad7b 100644 --- a/dotcom-rendering/src/layouts/CommentLayout.tsx +++ b/dotcom-rendering/src/layouts/CommentLayout.tsx @@ -589,7 +589,7 @@ export const CommentLayout = ({ article, NAV, format }: Props) => { imagesForAppsLightbox={[]} /> {showBodyEndSlot && ( - + { data-component="most-popular" > - + { - + { )}
- +
{ - + { - + { imagesForAppsLightbox={[]} /> {showBodyEndSlot && ( - + { data-component="most-popular" > - + { - + { max-width: 620px; `} > - + { data-component="most-popular" > - + { - + console.error(`HydratedLayout embedIframe - error: ${String(e)}`), ); - doStorybookHydration(); }, [serverArticle]); return ( diff --git a/dotcom-rendering/src/layouts/LiveLayout.tsx b/dotcom-rendering/src/layouts/LiveLayout.tsx index 751d3ebf3d5..060d2456203 100644 --- a/dotcom-rendering/src/layouts/LiveLayout.tsx +++ b/dotcom-rendering/src/layouts/LiveLayout.tsx @@ -415,7 +415,7 @@ export const LiveLayout = (props: WebProps | AppsProps) => { )}
{isApps && ( - + )} @@ -635,7 +635,7 @@ export const LiveLayout = (props: WebProps | AppsProps) => { justify-content: center; `} /> - + { leftColSize="wide" > - + { - + { data-component="most-popular" > - + { - + { imagesForAppsLightbox={[]} /> {showBodyEndSlot && ( - + { data-component="most-popular" > - + { - + {
{isApps && ( - + )} @@ -675,13 +675,13 @@ export const StandardLayout = (props: WebProps | AppProps) => { )} {isApps && ( - + )} {isWeb && showBodyEndSlot && ( - + { data-component="most-popular" > - + { /> - + {pinnedPost && onFirstPage && !filtered && ( <> - + @@ -144,7 +144,7 @@ export const LiveBlogRenderer = ({ isInLiveblogAdSlotTest={isInLiveblogAdSlotTest} /> {blocks.length > 4 && ( - + { const [isInUse, setIsInUse] = useState(); useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - isAdBlockInUse().then((blockerDetected) => { + void isAdBlockInUse().then((blockerDetected) => { setIsInUse(blockerDetected); }); }, []); diff --git a/dotcom-rendering/src/lib/useCountryCode.tsx b/dotcom-rendering/src/lib/useCountryCode.tsx new file mode 100644 index 00000000000..89f8762467d --- /dev/null +++ b/dotcom-rendering/src/lib/useCountryCode.tsx @@ -0,0 +1,19 @@ +import type { CountryCode } from '@guardian/libs'; +import { useState } from 'react'; +import { getLocaleCode } from './getCountryCode'; +import { useOnce } from './useOnce'; + +export const useCountryCode = (): CountryCode | undefined => { + const [localeCode, setLocaleCode] = useState(null); + useOnce(() => { + getLocaleCode() + .then((code) => { + setLocaleCode(code); + }) + .catch((e) => + console.error(`countryCodePromise - error: ${String(e)}`), + ); + }, []); + + return localeCode ?? undefined; +}; diff --git a/dotcom-rendering/src/lib/useHydrated.ts b/dotcom-rendering/src/lib/useHydrated.ts index ae5fd1ca333..2f99cca7bdc 100644 --- a/dotcom-rendering/src/lib/useHydrated.ts +++ b/dotcom-rendering/src/lib/useHydrated.ts @@ -1,9 +1,10 @@ import { useState } from 'react'; import { useOnce } from './useOnce'; -export const useHydrated = (): boolean => { +export const useHydrated = (hydrateInStorybook = true): boolean => { const [hydrated, setHydrated] = useState(false); useOnce(() => { + if (!hydrateInStorybook && window.IS_STORYBOOK) return; setHydrated(true); }, []); diff --git a/dotcom-rendering/src/lib/usePageViewId.tsx b/dotcom-rendering/src/lib/usePageViewId.tsx new file mode 100644 index 00000000000..39dba9c24d7 --- /dev/null +++ b/dotcom-rendering/src/lib/usePageViewId.tsx @@ -0,0 +1,5 @@ +import { isServer } from './isServer'; + +export const usePageViewId = (): string | undefined => { + return isServer ? undefined : window.guardian.config.ophan.pageViewId; +}; diff --git a/dotcom-rendering/src/lib/withSignInGateSlot.tsx b/dotcom-rendering/src/lib/withSignInGateSlot.tsx index de5c9d2105a..106fa61e4eb 100644 --- a/dotcom-rendering/src/lib/withSignInGateSlot.tsx +++ b/dotcom-rendering/src/lib/withSignInGateSlot.tsx @@ -41,7 +41,7 @@ export const withSignInGateSlot = ({ {/* Add the placeholder div after the second article element */} {i === 1 && (
- + void; @@ -84,7 +87,7 @@ declare global { YT?: { ImaManager: typeof ImaManager; }; - onYouTubeIframeAPIReady?: () => void + onYouTubeIframeAPIReady?: () => void; } } /* ~ this line is required as per TypeScript's global-modifying-module.d.ts instructions */