{
- 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 */