use-calendar-favicon.ts2.9 KBView on GitHub import { useEffect } from 'react';
import { calendarDayIconHref } from '@/lib/site-config';
/**
* Puts today's date on the tab icon while the calendar is open, the way Google Calendar does.
*
* The link element is created here rather than declared in the route's `meta` because the day has
* to come from the browser: the server renders the same HTML for everyone, so a date baked in at
* that point would be the server's day, and wrong for anyone far enough east or west. Adding it
* from an effect also keeps it out of React Router's hands — `<Meta />` owns the nodes it renders
* and would overwrite an href mutated underneath it.
*
* This is deliberately the *only* `rel="icon"` on the calendar route — its `meta` declares none.
* Browsers pick between competing icon links by their own rules, and a `rel="icon"` PNG carrying
* an explicit `sizes` beat this SVG often enough that the tab just kept showing the Cedar mark.
* With no icon link at all until this one lands, there is nothing to lose to; a browser too old
* for SVG icons ignores it and falls back to /favicon.ico, which is the Cedar mark anyway.
*/
export function useCalendarFavicon() {
useEffect(() => {
let link: HTMLLinkElement | null = null;
let rollover: ReturnType<typeof setTimeout>;
const paint = () => {
const now = new Date();
const href = calendarDayIconHref(now.getDate());
// Replace the element rather than re-pointing its href. Browsers re-read the icon when a
// rel="icon" link is inserted, but are unreliable about noticing an href change on one
// already in the document — which is why setting the href after appending showed nothing.
// Only when the day actually changed, though: `paint` also runs on every return to the
// tab, and re-inserting the same link makes the browser re-fetch and visibly blink the
// icon for no reason.
if (!link || link.getAttribute('href') !== href) {
link?.remove();
link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/svg+xml';
link.href = href;
document.head.appendChild(link);
}
// Turn the page at local midnight. The listener below is what actually catches most
// rollovers — a laptop that slept through 00:00 fires this timer late, if at all, so the
// date is also re-read whenever the tab becomes visible again.
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
clearTimeout(rollover);
rollover = setTimeout(paint, midnight.getTime() - now.getTime() + 1_000);
};
const repaintIfVisible = () => {
if (document.visibilityState === 'visible') paint();
};
paint();
document.addEventListener('visibilitychange', repaintIfVisible);
return () => {
clearTimeout(rollover);
document.removeEventListener('visibilitychange', repaintIfVisible);
link?.remove();
};
}, []);
}