> Why the static `<title>ARRFLIX</title>` patch wasn't enough, what the shim does,
> and how to extend it when Jellyfin starts overwriting more state on upgrade.
Last verified: 2026-05-08 against Jellyfin 10.10.3 web bundle.
---
## TL;DR
`/web/index.html` already says `<title>ARRFLIX</title>` and embeds our logo as the
favicon (data URL). The browser tab still showed "Jellyfin" + the Jellyfin teal
triangle because **Jellyfin's SPA overwrites `document.title` at runtime** as it
hydrates. Two compounding effects:
1.**SPA runtime overwrite (primary cause).** Bundle code does
`document.title = d.Ay.translateHtml(document.title ...)` and per-route updates
via `LibraryMenu.setTitle` / `Page.setTitle`. Whatever the static `<title>` is,
the SPA will replace it on first hydrate and again on every navigation.
2.**Service worker churn (secondary).**`serviceworker.js` registers and calls
`self.clients.claim()`. The shipped SW does NOT actually cache `index.html`
(it's a notification-only worker), but its presence still pins old clients
to whatever they had at first paint. Killing the SW once forces a clean reload.
Fix: a tiny self-contained shim in `<head>`, INLINE, BEFORE the bundle scripts.
It enforces the title and favicon both on hydrate and on every later DOM mutation.
---
## What the shim does
Lives in `web-overrides/index.html` between `<!-- ARRFLIX-SHIM-BEGIN -->` /
`-END` markers. Insertion is idempotent via `bin/inject-shim.py` (re-running the
script REPLACES the existing block instead of stacking duplicates).
Behaviour:
| Step | When | What |
|------|------|------|
| 1 | Page parse | `<script>` runs first thing inside `<head>`, before bundle. |
| 2 | DOM ready | `lockTitle()` strips/replaces "Jellyfin"; `lockFavicon()` re-pins icon hrefs to the data URL already in the page. |
| 3 | Mutation | A `MutationObserver` on `document.head` re-runs both lockers when the SPA tries to change `<title>` text or any `<link rel*="icon">` href. |
| 4 | Interval | A 1s `setInterval` is the safety net for late-binding navigations / route changes that bypass the head observer. |
| 5 | Once | `serviceWorker.getRegistrations()` finds anything pointing at `serviceworker.js` and calls `r.unregister()` so old clients reload fresh. `caches.keys()` is also flushed. |
The shim does NOT re-encode the logo. It reads the existing `data:image/png;base64,...`
href from the `<link rel="shortcut icon">` already in the page. So there's exactly
one copy of the logo data URL in the file.
---
## Files in play
```
/tmp/ARRFLIX/
bin/inject-shim.py # idempotent injector; the source of truth for shim content
web-overrides/index.html # bind-mounted to /jellyfin/jellyfin-web/index.html in the container