From 78edd658190e235d289309ee8f8a1fd67da727cd Mon Sep 17 00:00:00 2001 From: isaaclombardssw Date: Thu, 23 Jul 2026 10:32:17 +1000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20Resolve=20mobile=20header=20?= =?UTF-8?q?appearance=20server-side=20to=20fix=20CLS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header renders in the root layout, which has no access to the current page's data, so hiding the mobile country flag / Contact button ran in a client effect after hydration — reflowing the header and everything below it (CLS 0.133 on /events/ai-for-business-leaders, which hides both). Replace the post-hydration client context with a server-rendered marker: an opting-out page now emits a hidden [data-mm-hide-flag]/[data-mm-hide-contact] element, and unlayered CSS in styles.css hides the matching ssw.megamenu elements via :has() on first paint. SSR and hydration now match, and client-side navigation swaps the marker with the page so the header stays correct with no header-side JS. Chose CSS over the middleware x-pathname path the code comment documented: reading headers() in the root layout would opt every route out of static generation and conflicts with `dynamic = "force-static"` on the events route. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q8genJyZboXaa4vgYhJF4P --- .../events/[...filename]/eventsv2.tsx | 4 +- app/components/header-appearance.tsx | 51 +++++++------------ app/components/page-layout.tsx | 19 +++---- components/server/MegaMenuWrapper.tsx | 12 ++--- styles.css | 15 ++++++ 5 files changed, 48 insertions(+), 53 deletions(-) diff --git a/app/(events)/events/[...filename]/eventsv2.tsx b/app/(events)/events/[...filename]/eventsv2.tsx index 0a04498b2e..419fbf9bcc 100644 --- a/app/(events)/events/[...filename]/eventsv2.tsx +++ b/app/(events)/events/[...filename]/eventsv2.tsx @@ -1,5 +1,5 @@ "use client"; -import { useMobileHeaderAppearance } from "@/app/components/header-appearance"; +import { HeaderAppearanceMarker } from "@/app/components/header-appearance"; import { Blocks } from "@/components/blocks-renderer"; import { Container } from "@/components/util/container"; import { Section } from "@/components/util/section"; @@ -17,9 +17,9 @@ type EventsV2PageProps = { const EventsV2Page = memo( function EventsV2Page({ tinaProps }: EventsV2PageProps) { const { blocks, appearance } = tinaProps.data.eventsv2; - useMobileHeaderAppearance(appearance ?? null); return (
+
void; -}; -const HeaderAppearanceContext = createContext({ - mobile: {}, - setMobile: () => {}, -}); -export function HeaderAppearanceProvider({ - children, +// The header lives in the root layout and can't see the current page's data, so +// hiding the mobile flag/Contact button used to happen in a client effect AFTER +// hydration — reflowing the header and everything below it (CLS). Instead, the page +// server-renders this hidden marker; CSS in styles.css reads it via `:has()` and hides +// the elements on first paint, so SSR and hydration match. Client-side navigation swaps +// the marker in/out with the page, so the header updates without any header-side JS. +// (Middleware `x-pathname` + server resolution — the other documented fix — was rejected: +// reading headers() in the root layout breaks the `dynamic = "force-static"` routes.) +export function HeaderAppearanceMarker({ + appearance, }: { - children: React.ReactNode; + appearance?: MobileHeaderAppearance | null; }) { - const [mobile, setMobile] = useState({}); - const value = useMemo(() => ({ mobile, setMobile }), [mobile]); + if (!appearance?.hideFlag && !appearance?.hideContactButton) return null; return ( - - {children} - +