Skip to content

feat: add a banner area below the site header - #1564

Open
ngineer420 wants to merge 1 commit into
apache:mainfrom
ngineer420:fix/custom-header-below-site-header
Open

feat: add a banner area below the site header#1564
ngineer420 wants to merge 1 commit into
apache:mainfrom
ngineer420:fix/custom-header-below-site-header

Conversation

@ngineer420

@ngineer420 ngineer420 commented Jul 26, 2026

Copy link
Copy Markdown

Proposed Changes

Answer has a Header area in Admin → CSS and HTML. The client writes its content at the top of <body>:

const handleCustomHeader = (content) => {
  const el = document.body;
  renderCustomArea(el, CUSTOM_MARK_HEADER, 'afterbegin', content);
};

The server template writes it in the same place, before <div id="root">.

That position is above the nav bar and outside #root. The nav bar and the page are both inside #root, and the injected node is a sibling of #root. No stylesheet can move it below the nav. A banner under the site header is not possible today.

This PR adds a new Banner area. It does not move the Header area.

  • custom_banner is a new field on SiteCustomCssHTMLReq, beside custom_header and custom_sidebar.
  • pages/Layout renders <CustomBanner /> below <Header />.
  • Admin → CSS and HTML gets a Banner field, beside Head, Header, Footer and Sidebar.

ui/template/header.html, internal/controller/template_controller.go and ui/src/components/Customize/index.tsx are unchanged. The diff adds 136 lines and removes none.

Answers to the review

1. Breaking change. Removed. The Header area keeps its position and its behaviour, so existing sites see no change. The banner is a separate optional field, and it stays empty until an admin fills it in. CustomBanner renders nothing while the field is empty.

2. Scripts must run. CustomBanner re-creates the script nodes after it renders the slot, so the browser runs them. It runs them one time for each value, and it tracks the value it activated. One difference from ActivateScriptNodes: it leaves a data block alone, such as a script with type application/ld+json. The browser never runs a data block, and the IIFE wrapper would corrupt its content.

3. Head and footer refactor. Reverted. ui/src/components/Customize/index.tsx matches main byte for byte, and the meta[name="go-template"] skip stays.

Why the server template does not paint the banner

The first version of this PR painted the banner in header.html and read that markup back in the client. That cannot work, and it would have caused the same double execution described in point 3 above.

ui/src/index.tsx calls ReactDOM.createRoot, not hydrateRoot. App returns <InitialLoadingPlaceholder /> on its first render, because useMergeRoutes starts with an empty route list and fills it in an effect. That placeholder is the first commit of the root, and createRoot clears the container on the first commit. So React empties #root before Layout renders at all. Any banner the server painted inside #root is already gone, and the browser has already run its scripts once while it parsed the document.

The banner is therefore client-rendered only, in the same way as custom_sidebar and CustomSidebar. #spin-mask covers the viewport until React mounts, so a reader never sees the difference.

Checks

  • go build ./... and go vet pass.
  • tsc --noEmit, eslint and prettier --check pass on the changed files.
  • Swagger docs regenerated with swag v1.16.3. The ASF licence headers on docs/docs.go and docs/swagger.yaml are kept.

@LinkinStars
LinkinStars requested a review from robinv8 July 27, 2026 02:43

@robinv8 robinv8 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR — placing a banner below the site nav is a reasonable goal, and rendering it inside the React tree to survive hydration is the right approach. But after testing, I have to request changes:

  1. This is a breaking change, not just a fix. custom_header moves from the top of <body> to below the nav. Existing sites rely on the current position (top announcement bars, scripts that must run before #root). Please call this out explicitly in the PR description — or better, keep the current position and add a separate slot below the nav so existing users are unaffected.

  2. Scripts in the custom header no longer execute. The old path goes through ActivateScriptNodes, which re-creates <script> nodes so they run. dangerouslySetInnerHTML never executes scripts, so any header content containing JS silently breaks. Please activate script nodes after rendering the slot.

  3. Please revert the head/footer refactor. The go-template skip is not redundant: app settings are loaded before render (guard.ts:431), so on SSR pages the store is already populated at mount. The new unconditional injection clears the server-rendered markup and ActivateScriptNodes executes every script a second time (analytics double-counting, etc.). Keeping the skip avoids this.

Happy to re-review once these are addressed.

The Header area writes its content at the top of <body>. That position is
above the nav bar and outside #root. The nav bar and the page are both
inside #root, and the injected node is a sibling of it. No stylesheet can
move it below the nav, so a banner under the site header is not possible.

Add a separate Banner area. The Header area keeps its position and its
behaviour, so existing sites see no change.

- Add custom_banner to SiteCustomCssHTMLReq, to the customize store and to
  the Admin CSS and HTML form.
- Render CustomBanner below Header in pages/Layout. CustomSidebar renders
  custom_sidebar the same way.
- Re-create the script nodes in the slot after render, so the browser runs
  them. Run them one time for each value. Wrap only a classic script, so a
  data block such as application/ld+json keeps its content, and a module
  keeps its own scope.

The server template does not paint the banner. React uses createRoot, and
the first commit of the root clears everything inside #root before Layout
renders. A painted banner could not survive that, and its scripts would run
one extra time.
@ngineer420
ngineer420 force-pushed the fix/custom-header-below-site-header branch from 37843c5 to 33e0caf Compare August 26, 2026 03:50
@ngineer420 ngineer420 changed the title fix: custom header content cannot be placed below the site header feat: add a banner area below the site header Aug 26, 2026
@ngineer420

Copy link
Copy Markdown
Author

@robinv8 Thanks for the detailed review. All three points are addressed, and the approach changed.

The PR no longer moves custom_header. It adds a separate custom_banner field and renders it below the site header. ui/src/components/Customize/index.tsx, ui/template/header.html and internal/controller/template_controller.go are now unchanged from main. The diff adds 130 lines and removes none.

  1. Breaking change — gone. The Header area keeps its position and its behaviour. The banner is a separate optional field, and it renders nothing until an admin fills it in.
  2. ScriptsCustomBanner re-creates the script nodes after it renders the slot, and runs them one time for each value. It wraps only a classic script, so a data block such as application/ld+json keeps its content.
  3. Head and footer refactor — reverted. Customize/index.tsx matches main byte for byte, and the meta[name="go-template"] skip stays.

Your third point also showed that the first version was wrong in a second way. App returns <InitialLoadingPlaceholder /> on its first render, because useMergeRoutes starts with an empty route list. That placeholder is the first commit of the root, and createRoot clears the container on the first commit. So React empties #root before Layout renders at all. A banner painted into #root by the server cannot survive that, and the browser has already run its scripts once during parse. The banner is therefore client-rendered only, in the same way as custom_sidebar and CustomSidebar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants