diff --git a/ui/src/components/Customize/index.tsx b/ui/src/components/Customize/index.tsx index 7da85b057..13387377f 100644 --- a/ui/src/components/Customize/index.tsx +++ b/ui/src/components/Customize/index.tsx @@ -23,7 +23,9 @@ import { useLocation } from 'react-router-dom'; import { customizeStore } from '@/stores'; const CUSTOM_MARK_HEAD = 'customize_head'; -const CUSTOM_MARK_HEADER = 'customize_header'; +// The header area is NOT handled here. It is rendered by pages/Layout as part +// of the React tree, because it lives inside #root and anything injected there +// is destroyed when React takes over -- see the comment on the slot. const CUSTOM_MARK_FOOTER = 'customize_footer'; const makeMarker = (mark) => { @@ -104,34 +106,28 @@ const handleCustomHead = (content) => { renderCustomArea(el, CUSTOM_MARK_HEAD, 'beforeend', content); }; -const handleCustomHeader = (content) => { - const el = document.body; - renderCustomArea(el, CUSTOM_MARK_HEADER, 'afterbegin', content); -}; - const handleCustomFooter = (content) => { const el = document.body; renderCustomArea(el, CUSTOM_MARK_FOOTER, 'beforeend', content); }; const Index: FC = () => { - const { custom_head, custom_header, custom_footer } = customizeStore( - (state) => state, - ); + const { custom_head, custom_footer } = customizeStore((state) => state); const { pathname } = useLocation(); useEffect(() => { - const isSeo = document.querySelector('meta[name="go-template"]'); - if (!isSeo) { - setTimeout(() => { - handleCustomHead(custom_head); - }, 1000); - handleCustomHeader(custom_header); - handleCustomFooter(custom_footer); - } else { - isSeo.remove(); - } - }, [custom_head, custom_header, custom_footer]); + // Drop the server-render marker, then refresh the head and footer areas + // regardless of whether the server had already written them. Both live + // outside #root, so re-running is a replace, not a duplicate: + // renderCustomArea brackets its output with comment markers and clears what + // lies between them first. + document.querySelector('meta[name="go-template"]')?.remove(); + + setTimeout(() => { + handleCustomHead(custom_head); + }, 1000); + handleCustomFooter(custom_footer); + }, [custom_head, custom_footer]); useEffect(() => { /** diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index 048ca812e..374665598 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { FC, memo, useEffect } from 'react'; +import { FC, memo, useEffect, useState } from 'react'; import { Outlet, useLocation, ScrollRestoration } from 'react-router-dom'; import { HelmetProvider } from 'react-helmet-async'; @@ -30,6 +30,7 @@ import { errorCodeStore, siteSecurityStore, themeSettingStore, + customizeStore, } from '@/stores'; import { Header, @@ -59,6 +60,14 @@ const Layout: FC = () => { const { show: showLoginToContinueModal } = loginToContinueStore(); const { data: notificationData } = useQueryNotificationStatus(); const layout = themeSettingStore((state) => state.layout); + const storedCustomHeader = customizeStore((state) => state.custom_header); + // Seeded from whatever the server rendered into the slot. The store is filled + // by an API call, so it is empty on the first render, and rendering that empty + // would blank the server's markup for as long as the request takes. + const [paintedCustomHeader] = useState( + () => document.getElementById('custom-header-slot')?.innerHTML ?? '', + ); + const customHeader = storedCustomHeader || paintedCustomHeader; useEffect(() => { // handle footnote links const fixFootnoteLinks = () => { @@ -211,6 +220,19 @@ const Layout: FC = () => { revalidateOnFocus: false, }}>
+ {/* Custom header content, below the site header rather than above it, + which is where a site wants a banner. + + Rendered here rather than injected after mount: React owns everything + inside #root, so injected markup is destroyed when it takes over, and + re-adding it afterwards makes the banner visibly appear, vanish and + return. Rendering it as part of the tree means the server's markup and + React's output are the same thing. */} +
- - {{if .HeaderCode }} {{.HeaderCode | templateHTML}} {{end}} -
@@ -187,6 +184,15 @@
+ +
+ + {{if .HeaderCode }} {{.HeaderCode | templateHTML}} {{end}} + +