Fix global CSS wipe: inject head/body code via DOM, not <head dangerouslySetInnerHTML>
Rendering <head dangerouslySetInnerHTML={headCode}> in the App Router root
layout let the manual <head> overwrite the React-hoisted <link rel=stylesheet>
during hydration — all Tailwind styles dropped for real users once JS ran
(headless snapshots taken before hydration looked fine). headCode held the
Yandex.Metrika snippet. Replace with HtmlInjector client component that appends
the code through the DOM post-hydration, recreating <script> nodes so they run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-6
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Fira_Mono } from "next/font/google";
|
import { Fira_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { getSettings } from "@/lib/settings";
|
import { getSettings } from "@/lib/settings";
|
||||||
|
import { HtmlInjector } from "@/components/layout/html-injector";
|
||||||
|
|
||||||
const firaMono = Fira_Mono({
|
const firaMono = Fira_Mono({
|
||||||
weight: ["400", "500", "700"],
|
weight: ["400", "500", "700"],
|
||||||
@@ -28,14 +29,13 @@ export default async function RootLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="ru" className={`${firaMono.variable} h-full antialiased`}>
|
<html lang="ru" className={`${firaMono.variable} h-full antialiased`}>
|
||||||
{settings.headCode ? (
|
|
||||||
<head dangerouslySetInnerHTML={{ __html: settings.headCode }} />
|
|
||||||
) : null}
|
|
||||||
<body className="min-h-full flex flex-col">
|
<body className="min-h-full flex flex-col">
|
||||||
|
{/* Код внедрения инжектится через DOM после гидрации (HtmlInjector),
|
||||||
|
а НЕ через <head dangerouslySetInnerHTML> — иначе ручной <head>
|
||||||
|
затирает хойстнутый React'ом <link rel="stylesheet"> и слетает дизайн. */}
|
||||||
|
{settings.headCode ? <HtmlInjector html={settings.headCode} target="head" /> : null}
|
||||||
{children}
|
{children}
|
||||||
{settings.bodyCode ? (
|
{settings.bodyCode ? <HtmlInjector html={settings.bodyCode} target="body" /> : null}
|
||||||
<div dangerouslySetInnerHTML={{ __html: settings.bodyCode }} />
|
|
||||||
) : null}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Безопасно инжектит произвольный HTML (код внедрения из Настроек) в <head> или
|
||||||
|
* <body> ЧЕРЕЗ DOM после гидрации.
|
||||||
|
*
|
||||||
|
* Почему не <head dangerouslySetInnerHTML>: в App Router + React 19 ручной <head>
|
||||||
|
* при гидрации затирает хойстнутый React'ом <link rel="stylesheet"> → весь дизайн
|
||||||
|
* слетает. Этот компонент добавляет узлы программно, не трогая управление head,
|
||||||
|
* поэтому стили остаются на месте. <script> из innerHTML не исполняются — поэтому
|
||||||
|
* пересоздаём их как настоящие script-элементы.
|
||||||
|
*/
|
||||||
|
export function HtmlInjector({ html, target }: { html: string; target: "head" | "body" }) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!html) return;
|
||||||
|
const dest = target === "head" ? document.head : document.body;
|
||||||
|
const tpl = document.createElement("template");
|
||||||
|
tpl.innerHTML = html;
|
||||||
|
|
||||||
|
const added: Node[] = [];
|
||||||
|
Array.from(tpl.content.childNodes).forEach((node) => {
|
||||||
|
let toAppend: Node;
|
||||||
|
if (node.nodeName === "SCRIPT") {
|
||||||
|
const orig = node as HTMLScriptElement;
|
||||||
|
const s = document.createElement("script");
|
||||||
|
for (const attr of Array.from(orig.attributes)) s.setAttribute(attr.name, attr.value);
|
||||||
|
s.textContent = orig.textContent;
|
||||||
|
toAppend = s;
|
||||||
|
} else {
|
||||||
|
toAppend = node.cloneNode(true);
|
||||||
|
}
|
||||||
|
dest.appendChild(toAppend);
|
||||||
|
added.push(toAppend);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
added.forEach((n) => n.parentNode?.removeChild(n));
|
||||||
|
};
|
||||||
|
}, [html, target]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user