web: signed-out root visits redirect to the home sign-in page (#732)
* web: signed-out root visits redirect to the home sign-in page A bare browser visit with no session showed a near-empty landing card (on a sealed instance, deliberately nameless). Members' spaces live in their home vault, so send them there: showNoInvite now location.replace()s to __HOME_ORIGIN__. The paste-to-join card survives where it's load-bearing — the installed Home-Screen app (its only way to consume an invite link) — and via ?stay=1 anywhere; native webviews were never routed here. Dev pages without the substitution keep the card (origin sanity check). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * web: pre-paint the signed-out root bounce (no boot flash before the hop) The redirect stays client-side by necessity — the session lives in localStorage the server deliberately can't see — but firing it from showNoInvite meant a full shell boot rendered before the hop. An inline nonced head script now runs the same gate before first paint, so a signed-out visit leaves without rendering anything; showNoInvite keeps the check as the backstop for storage-denied/corrupt-session edges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
3 files changed,
+41
−0
changelog/2026-07-11-root-redirect.md+4 −0web/pages/index.html+22 −0web/static/shell.js+15 −0
diff --git a/changelog/2026-07-11-root-redirect.md b/changelog/2026-07-11-root-redirect.md| new file mode 100644 |
| index 0000000..152a311 |
| --- /dev/null |
| +++ b/changelog/2026-07-11-root-redirect.md |
| @@ -0,0 +1,4 @@ |
| +🤖 Opening a space's web address when you're not signed in now takes you |
| +straight to the Eleven sign-in page, where your saved spaces live — instead of |
| +a bare card. The installed Home-Screen app keeps its paste-your-invite-link |
| +screen, and invite links themselves work exactly as before. |
diff --git a/web/pages/index.html b/web/pages/index.html| index bac8d77..80057c3 100644 |
| --- a/web/pages/index.html |
| +++ b/web/pages/index.html |
| @@ -23,6 +23,28 @@ |
| <script nonce="__CSP_NONCE__"> |
| window.__homeOrigin = "__HOME_ORIGIN__"; |
| </script> |
| +<!-- Pre-paint bounce: a signed-out browser visit belongs on home's sign-in |
| + page. This must be client-side — the session lives in localStorage, which |
| + the server (deliberately) cannot see, so an HTTP redirect would bounce |
| + signed-in members too. Running it here, before first paint, makes it feel |
| + like one: nothing renders before the hop. shell.js's showNoInvite keeps |
| + the same check as a backstop (corrupt session, storage denied). Stays put: |
| + native webviews (__lchatEmbed, injected at documentStart), the installed |
| + Home-Screen app (paste-to-join is its only way to consume an invite |
| + link), ?stay=1, restore returns from home, and unsubstituted dev pages. --> |
| +<script nonce="__CSP_NONCE__"> |
| + (function () { |
| + try { |
| + if (window.__lchatEmbed) return; |
| + if (matchMedia("(display-mode: standalone)").matches || navigator.standalone === true) return; |
| + if (localStorage.getItem("lchat_session")) return; |
| + if (location.hash.indexOf("lchat_restore=") !== -1) return; |
| + if (new URLSearchParams(location.search).has("stay")) return; |
| + var home = window.__homeOrigin || ""; |
| + if (home.indexOf("http") === 0) location.replace(home); |
| + } catch (e) {} /* storage denied etc.: route() decides after boot */ |
| + })(); |
| +</script> |
| <link rel="stylesheet" href="/static/style.css" /> |
| <link rel="manifest" href="/manifest.webmanifest" /> |
| <link rel="icon" href="/static/icon-192.png" /> |
diff --git a/web/static/shell.js b/web/static/shell.js| index eff0d5c..5a4acb0 100644 |
| --- a/web/static/shell.js |
| +++ b/web/static/shell.js |
| @@ -10287,6 +10287,21 @@ function parseLink(text) { |
| } |
| function showNoInvite() { |
| + // A bare browser visit with no session belongs on the home server's sign-in |
| + // page — members recover their spaces there, new people learn what Eleven |
| + // is, and the instance's landing reveals nothing (which sealed branding |
| + // makes literal). The card below survives for the one surface that NEEDS |
| + // it: the installed Home-Screen app consumes invite links by paste — it has |
| + // no other way in — and ?stay=1 keeps it reachable everywhere (debugging, |
| + // and a deliberate "no, this instance's page" bookmark). Native webviews |
| + // never get here (route() hands embed to bootEmbed first). The origin check |
| + // also skips a dev page served without __HOME_ORIGIN__ substitution. |
| + const standalone = |
| + matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true; |
| + const home = window.__homeOrigin || ""; |
| + if (!standalone && home.startsWith("http") && !new URLSearchParams(location.search).has("stay")) { |
| + return location.replace(home); |
| + } |
| hideLoading(); |
| app.appendChild(tpl("noinvite-tpl")); |
| // Public branding: reveal the server photo once /api/group confirms one is |