Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 16 additions & 20 deletions ui/src/components/Customize/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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(() => {
/**
Expand Down
24 changes: 23 additions & 1 deletion ui/src/pages/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -30,6 +30,7 @@ import {
errorCodeStore,
siteSecurityStore,
themeSettingStore,
customizeStore,
} from '@/stores';
import {
Header,
Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -211,6 +220,19 @@ const Layout: FC = () => {
revalidateOnFocus: false,
}}>
<Header />
{/* 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. */}
<div
id="custom-header-slot"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: customHeader || '' }}
/>
<div
className={classnames(
'position-relative page-wrap d-flex flex-column flex-fill',
Expand Down
12 changes: 9 additions & 3 deletions ui/template/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,6 @@
</head>

<body>
<!--customize_header-->
{{if .HeaderCode }} {{.HeaderCode | templateHTML}} {{end}}
<!--customize_header-->
<div id="root">

<div id="spin-mask">
Expand Down Expand Up @@ -187,6 +184,15 @@
</div>
</div>
</nav>
<!-- Custom header content goes below the site header, matching the slot
the client renders in pages/Layout. Emitting it at the top of <body>
put it above the nav bar and outside #root, where no stylesheet could
reach it, so a banner under the site's own header was impossible. -->
<div id="custom-header-slot">
<!--customize_header-->
{{if .HeaderCode }} {{.HeaderCode | templateHTML}} {{end}}
<!--customize_header-->
</div>
<div class="position-relative page-wrap d-flex flex-column flex-fill">
<div class="d-flex">
<div class="position-sticky px-3 border-end pt-4 d-none d-xl-block" id="pcSideNav">
Expand Down