Skip to content

fix(core): inline custom CSS into experience HTML head to prevent style flash#8917

Draft
darcyYe wants to merge 5 commits into
logto-io:masterfrom
darcyYe:yemq-experience-custom-css-ssr-no-fouc
Draft

fix(core): inline custom CSS into experience HTML head to prevent style flash#8917
darcyYe wants to merge 5 commits into
logto-io:masterfrom
darcyYe:yemq-experience-custom-css-ssr-no-fouc

Conversation

@darcyYe
Copy link
Copy Markdown
Contributor

@darcyYe darcyYe commented May 31, 2026

Summary

When custom CSS is configured for the sign-in experience, the hosted page briefly rendered the built-in styles before the custom CSS applied (a flash of built-in content). Custom CSS was only injected client-side via react-helmet, which mutates <head> asynchronously after the page had already painted.

The experience SSR middleware now inlines the tenant's customCss into a <style data-custom-css> element in the served HTML <head>, so custom CSS is part of the cascade on the first paint.

The pre-existing client-side react-helmet <style> (in AppMeta.tsx) is intentionally kept and does not reintroduce the flash: the server-inlined <style> already styles the first paint, and the helmet tag — added later with identical CSS — produces no visible repaint. It is kept because it is the only custom-CSS path for live preview: the server deliberately skips inlining in preview mode (?preview=true), where the console iframe pushes styling live via postMessage. On the real page it re-asserts the same CSS (a harmless duplicate that also acts as a precedence backstop). Removing it would break live preview without improving the fix.

  • Inline customCss into <head> before the SSR placeholder substitution, so the </head> match only targets the genuine document head.
  • Skip the inline in preview mode (?preview=true), where the console iframe drives styling live via postMessage + react-helmet.
  • Escape the </style> sequence in custom CSS so it cannot terminate the <style> element early.
  • Serialize the embedded window.logtoSsr data through a new serializeSsrData helper that escapes </>/&, preventing a </script> in tenant data (custom CSS or custom content) from breaking out of the inline script.

This affects the production/self-hosted (and Cloud) build path; in development the experience is proxied to Vite and the middleware is a no-op.

Testing

Unit tests

Checklist

  • .changeset
  • unit tests
  • integration tests
  • necessary TSDoc comments

@github-actions
Copy link
Copy Markdown

COMPARE TO master

Total Size Diff 📈 +6.35 KB

Diff by File
Name Diff
.changeset/gentle-otters-cheer.md 📈 +653 Bytes
packages/core/src/middleware/koa-experience-ssr.test.ts 📈 +3.79 KB
packages/core/src/middleware/koa-experience-ssr.ts 📈 +1.92 KB

@darcyYe darcyYe force-pushed the yemq-experience-custom-css-ssr-no-fouc branch from a130e69 to d275df2 Compare June 3, 2026 11:44
@github-actions github-actions Bot added size/m and removed size/m labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

1 participant