mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-26 00:01:19 +00:00
fix(favicon): stop tearing out Next.js's managed icon link
The badge hook was calling removeCurrentFavicons() — document.head
.querySelectorAll("link[rel~='icon']").forEach(l => l.remove()) —
which also removed the link that Next.js renders from app/icon.svg
and manages via its metadata reconciler. On the next render, React's
commit phase tried to remove the now-missing node and crashed with
"can't access property 'removeChild', finishedRoot.parentNode is
null".
Leave Next.js's icon alone. The hook now upserts its own link tagged
data-dynamic-favicon and cleans up only that one on unmount / count
returning to 0. Chromium picks the last matching rel="icon" link, so
the badge still appears there; other browsers fall back to the static
icon, which is an acceptable degradation.
This commit is contained in:
+37
-20
@@ -3,26 +3,30 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
const SIZE = 32;
|
||||
const DYNAMIC_ATTR = "data-dynamic-favicon";
|
||||
|
||||
export function useFaviconBadge(count: number) {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
const originalHref = useRef<string | null>(null);
|
||||
const countRef = useRef(count);
|
||||
countRef.current = count;
|
||||
|
||||
useEffect(() => {
|
||||
const link = document.querySelector<HTMLLinkElement>("link[rel~='icon']");
|
||||
if (!link?.href) return;
|
||||
|
||||
originalHref.current = link.href;
|
||||
// Grab the base favicon href from whatever the document currently has
|
||||
// (Next.js renders one from app/icon.svg). We NEVER remove or edit
|
||||
// that link — Next.js's metadata reconciler owns it and pulling it
|
||||
// out from under React crashes reconciliation with
|
||||
// "parentNode is null". Our badge lives on its own link tagged with
|
||||
// data-dynamic-favicon.
|
||||
const baseLink = document.querySelector<HTMLLinkElement>("link[rel~='icon']:not([data-dynamic-favicon])");
|
||||
if (!baseLink?.href) return;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = SIZE;
|
||||
canvas.height = SIZE;
|
||||
canvasRef.current = canvas;
|
||||
|
||||
fetch(link.href)
|
||||
fetch(baseLink.href)
|
||||
.then((r) => r.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
@@ -41,13 +45,15 @@ export function useFaviconBadge(count: number) {
|
||||
.catch(() => {
|
||||
/* favicon not loadable — badge will draw without base icon */
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearDynamicFavicon();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (count <= 0) {
|
||||
if (originalHref.current) {
|
||||
setFavicon(originalHref.current, "image/svg+xml");
|
||||
}
|
||||
clearDynamicFavicon();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,19 +108,30 @@ function applyBadge(
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillText(label, x + padding, y + padding / 2);
|
||||
|
||||
setFavicon(canvas.toDataURL("image/png"));
|
||||
setDynamicFavicon(canvas.toDataURL("image/png"));
|
||||
}
|
||||
|
||||
function removeCurrentFavicons() {
|
||||
const links = document.querySelectorAll<HTMLLinkElement>("link[rel~='icon']");
|
||||
links.forEach((link) => link.remove());
|
||||
/**
|
||||
* Upsert a dynamic favicon link marked with data-dynamic-favicon. The
|
||||
* app/icon.svg managed by Next.js is left alone. Browsers pick the
|
||||
* last matching link for rel="icon", so the dynamic link takes
|
||||
* precedence on Chromium; Firefox/Safari fall back to the static icon,
|
||||
* which is fine — the count-based badge remains a Chromium nicety.
|
||||
*/
|
||||
function setDynamicFavicon(href: string) {
|
||||
let link = document.querySelector<HTMLLinkElement>(`link[${DYNAMIC_ATTR}]`);
|
||||
if (!link) {
|
||||
link = document.createElement("link");
|
||||
link.setAttribute(DYNAMIC_ATTR, "");
|
||||
link.rel = "icon";
|
||||
link.type = "image/png";
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
if (link.href !== href) {
|
||||
link.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
export function setFavicon(href: string, type: string = "image/png") {
|
||||
removeCurrentFavicons();
|
||||
const link = document.createElement("link");
|
||||
link.rel = "icon";
|
||||
link.type = type;
|
||||
link.href = href;
|
||||
document.head.appendChild(link);
|
||||
function clearDynamicFavicon() {
|
||||
document.querySelector(`link[${DYNAMIC_ATTR}]`)?.remove();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user