From fdcf668e06d36e1b60da69c006b87b8c9a5beee4 Mon Sep 17 00:00:00 2001
From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com>
Date: Thu, 16 Oct 2025 11:59:19 +0530
Subject: [PATCH 1/2] fix: name of route in history
---
frontend/src/components/layouts/AppLayout.tsx | 62 ++++++++++++++++++-
1 file changed, 61 insertions(+), 1 deletion(-)
diff --git a/frontend/src/components/layouts/AppLayout.tsx b/frontend/src/components/layouts/AppLayout.tsx
index beced9ff3..801e43417 100644
--- a/frontend/src/components/layouts/AppLayout.tsx
+++ b/frontend/src/components/layouts/AppLayout.tsx
@@ -1,4 +1,5 @@
-import { Outlet } from "react-router-dom";
+import React from "react";
+import { Outlet, useMatches } from "react-router-dom";
import { AppSidebar } from "src/components/AppSidebar";
import { Banner } from "src/components/Banner";
@@ -22,6 +23,65 @@ function AppLayoutInner() {
useRemoveSuccessfulChannelOrder();
useNotifyReceivedPayments();
+ // Update document.title and the history entry when the route changes.
+ // This ensures the browser's history entries include a proper title
+ // (fixes: back gesture / long-press back showing empty/incorrect titles).
+ const matches = useMatches();
+ React.useEffect(() => {
+ try {
+ // Attempt to derive a title from route handles (crumb) or matched route
+ // fallback to app name. Use a small typed helper instead of `any` to
+ // satisfy lint rules.
+ const getCrumbFromHandle = (
+ handle: unknown
+ ): string | string[] | null => {
+ if (handle && typeof handle === "object") {
+ const h = handle as { crumb?: unknown };
+ if (typeof h.crumb === "function") {
+ try {
+ return (h.crumb as () => string | string[])();
+ } catch (err) {
+ return null;
+ }
+ }
+ }
+ return null;
+ };
+
+ const crumbTitle =
+ matches
+ .map((m) => getCrumbFromHandle(m.handle))
+ .filter(Boolean)
+ .pop() || "Alby Hub";
+
+ const title = Array.isArray(crumbTitle)
+ ? crumbTitle.join(" - ")
+ : crumbTitle;
+
+ // Set document title
+ document.title = title as string;
+
+ // Replace current history state to include title in state (some browsers show
+ // history entry title from state). We keep the existing state but add _title.
+ try {
+ const state =
+ history.state && typeof history.state === "object"
+ ? { ...history.state }
+ : {};
+ if (state && state._title !== title) {
+ state._title = title;
+ history.replaceState(state, title as string, window.location.href);
+ }
+ } catch (err) {
+ // ignore replaceState errors in weird environments
+ // eslint-disable-next-line no-console
+ console.debug("history.replaceState failed", err);
+ }
+ } catch (err) {
+ console.error("Failed to compute page title", err);
+ }
+ }, [matches]);
+
if (!info) {
return null;
}
From 29c010d22163c8a9f3f8035c339c86bca914eba3 Mon Sep 17 00:00:00 2001
From: Krrish Sehgal <133865424+krrish-sehgal@users.noreply.github.com>
Date: Mon, 20 Oct 2025 13:48:33 +0530
Subject: [PATCH 2/2] fix: add titles to route handles and use them instead of
bread crumbs
---
frontend/src/components/layouts/AppLayout.tsx | 64 +-------------
frontend/src/hooks/useDocumentTitle.ts | 49 +++++++++++
frontend/src/routes.tsx | 88 ++++++++++++-------
3 files changed, 108 insertions(+), 93 deletions(-)
create mode 100644 frontend/src/hooks/useDocumentTitle.ts
diff --git a/frontend/src/components/layouts/AppLayout.tsx b/frontend/src/components/layouts/AppLayout.tsx
index 801e43417..a6b06cac1 100644
--- a/frontend/src/components/layouts/AppLayout.tsx
+++ b/frontend/src/components/layouts/AppLayout.tsx
@@ -1,5 +1,4 @@
-import React from "react";
-import { Outlet, useMatches } from "react-router-dom";
+import { Outlet } from "react-router-dom";
import { AppSidebar } from "src/components/AppSidebar";
import { Banner } from "src/components/Banner";
@@ -10,6 +9,7 @@ import {
useCommandPaletteContext,
} from "src/contexts/CommandPaletteContext";
import { useBanner } from "src/hooks/useBanner";
+import { useDocumentTitle } from "src/hooks/useDocumentTitle";
import { useInfo } from "src/hooks/useInfo";
import { useNotifyReceivedPayments } from "src/hooks/useNotifyReceivedPayments";
import { useRemoveSuccessfulChannelOrder } from "src/hooks/useRemoveSuccessfulChannelOrder";
@@ -22,65 +22,7 @@ function AppLayoutInner() {
useRemoveSuccessfulChannelOrder();
useNotifyReceivedPayments();
-
- // Update document.title and the history entry when the route changes.
- // This ensures the browser's history entries include a proper title
- // (fixes: back gesture / long-press back showing empty/incorrect titles).
- const matches = useMatches();
- React.useEffect(() => {
- try {
- // Attempt to derive a title from route handles (crumb) or matched route
- // fallback to app name. Use a small typed helper instead of `any` to
- // satisfy lint rules.
- const getCrumbFromHandle = (
- handle: unknown
- ): string | string[] | null => {
- if (handle && typeof handle === "object") {
- const h = handle as { crumb?: unknown };
- if (typeof h.crumb === "function") {
- try {
- return (h.crumb as () => string | string[])();
- } catch (err) {
- return null;
- }
- }
- }
- return null;
- };
-
- const crumbTitle =
- matches
- .map((m) => getCrumbFromHandle(m.handle))
- .filter(Boolean)
- .pop() || "Alby Hub";
-
- const title = Array.isArray(crumbTitle)
- ? crumbTitle.join(" - ")
- : crumbTitle;
-
- // Set document title
- document.title = title as string;
-
- // Replace current history state to include title in state (some browsers show
- // history entry title from state). We keep the existing state but add _title.
- try {
- const state =
- history.state && typeof history.state === "object"
- ? { ...history.state }
- : {};
- if (state && state._title !== title) {
- state._title = title;
- history.replaceState(state, title as string, window.location.href);
- }
- } catch (err) {
- // ignore replaceState errors in weird environments
- // eslint-disable-next-line no-console
- console.debug("history.replaceState failed", err);
- }
- } catch (err) {
- console.error("Failed to compute page title", err);
- }
- }, [matches]);
+ useDocumentTitle();
if (!info) {
return null;
diff --git a/frontend/src/hooks/useDocumentTitle.ts b/frontend/src/hooks/useDocumentTitle.ts
new file mode 100644
index 000000000..7349cd340
--- /dev/null
+++ b/frontend/src/hooks/useDocumentTitle.ts
@@ -0,0 +1,49 @@
+import React from "react";
+import { useMatches } from "react-router-dom";
+
+/**
+ * Custom hook to manage document.title based on route handles.
+ * This ensures the browser's history entries include a proper title
+ * (fixes: back gesture / long-press back showing empty/incorrect titles).
+ *
+ * Looks for a `title` property in route handles (string or function),
+ * falling back to "Alby Hub" if not present.
+ */
+export function useDocumentTitle() {
+ const matches = useMatches();
+
+ React.useEffect(() => {
+ try {
+ // Extract title from route handles. Use a typed helper to get the title
+ // from the handle object (if present).
+ const getTitleFromHandle = (handle: unknown): string | null => {
+ if (handle && typeof handle === "object") {
+ const h = handle as { title?: unknown };
+ if (typeof h.title === "string") {
+ return h.title;
+ }
+ if (typeof h.title === "function") {
+ try {
+ return (h.title as () => string)();
+ } catch (err) {
+ return null;
+ }
+ }
+ }
+ return null;
+ };
+
+ // Find the last (most specific) route with a title, or default to "Alby Hub"
+ const routeTitle =
+ matches
+ .map((m) => getTitleFromHandle(m.handle))
+ .filter(Boolean)
+ .pop() || "Alby Hub";
+
+ // Set document title
+ document.title = routeTitle;
+ } catch (err) {
+ console.error("Failed to compute page title", err);
+ }
+ }, [matches]);
+}
diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx
index 5ed1436a1..57e2faef9 100644
--- a/frontend/src/routes.tsx
+++ b/frontend/src/routes.tsx
@@ -102,7 +102,7 @@ const routes = [
{
path: "home",
element: ,
- handle: { crumb: () => "Dashboard" },
+ handle: { crumb: () => "Dashboard", title: "Dashboard" },
children: [
{
index: true,
@@ -113,7 +113,7 @@ const routes = [
{
path: "wallet",
element: ,
- handle: { crumb: () => "Wallet" },
+ handle: { crumb: () => "Wallet", title: "Wallet" },
children: [
{
index: true,
@@ -121,7 +121,7 @@ const routes = [
},
{
path: "swap",
- handle: { crumb: () => "Swap" },
+ handle: { crumb: () => "Swap", title: "Swap" },
children: [
{
index: true,
@@ -143,24 +143,30 @@ const routes = [
},
{
path: "receive",
- handle: { crumb: () => "Receive" },
+ handle: { crumb: () => "Receive", title: "Receive" },
children: [
{
index: true,
element: ,
},
{
- handle: { crumb: () => "Receive On-chain" },
+ handle: {
+ crumb: () => "Receive On-chain",
+ title: "Receive On-chain",
+ },
path: "onchain",
element: ,
},
{
- handle: { crumb: () => "Invoice" },
+ handle: { crumb: () => "Invoice", title: "Invoice" },
path: "invoice",
element: ,
},
{
- handle: { crumb: () => "BOLT-12 Offer" },
+ handle: {
+ crumb: () => "BOLT-12 Offer",
+ title: "BOLT-12 Offer",
+ },
path: "offer",
element: ,
},
@@ -168,7 +174,7 @@ const routes = [
},
{
path: "send",
- handle: { crumb: () => "Send" },
+ handle: { crumb: () => "Send", title: "Send" },
children: [
{
index: true,
@@ -203,24 +209,27 @@ const routes = [
{
path: "sign-message",
element: ,
- handle: { crumb: () => "Sign Message" },
+ handle: { crumb: () => "Sign Message", title: "Sign Message" },
},
{
path: "node-alias",
element: ,
- handle: { crumb: () => "Node Alias" },
+ handle: { crumb: () => "Node Alias", title: "Node Alias" },
},
{
path: "withdraw",
element: ,
- handle: { crumb: () => "Withdraw On-Chain Balance" },
+ handle: {
+ crumb: () => "Withdraw On-Chain Balance",
+ title: "Withdraw On-Chain Balance",
+ },
},
],
},
{
path: "settings",
element: ,
- handle: { crumb: () => "Settings" },
+ handle: { crumb: () => "Settings", title: "Settings" },
children: [
{
path: "",
@@ -233,22 +242,25 @@ const routes = [
{
path: "about",
element: ,
- handle: { crumb: () => "About" },
+ handle: { crumb: () => "About", title: "About" },
},
{
path: "auto-unlock",
element: ,
- handle: { crumb: () => "Auto Unlock" },
+ handle: { crumb: () => "Auto Unlock", title: "Auto Unlock" },
},
{
path: "change-unlock-password",
element: ,
- handle: { crumb: () => "Unlock Password" },
+ handle: {
+ crumb: () => "Unlock Password",
+ title: "Change Unlock Password",
+ },
},
{
path: "backup",
element: ,
- handle: { crumb: () => "Backup" },
+ handle: { crumb: () => "Backup", title: "Backup" },
},
{
path: "node-migrate",
@@ -273,7 +285,7 @@ const routes = [
{
path: "apps",
element: ,
- handle: { crumb: () => "Connections" },
+ handle: { crumb: () => "Connections", title: "Connections" },
children: [
{
index: true,
@@ -286,7 +298,7 @@ const routes = [
{
path: "new",
element: ,
- handle: { crumb: () => "New App" },
+ handle: { crumb: () => "New App", title: "New Connection" },
},
{
path: "cleanup",
@@ -297,7 +309,7 @@ const routes = [
{
path: "sub-wallets",
element: ,
- handle: { crumb: () => "Sub-wallets" },
+ handle: { crumb: () => "Sub-wallets", title: "Sub-wallets" },
children: [
{
@@ -317,7 +329,7 @@ const routes = [
{
path: "internal-apps",
element: ,
- handle: { crumb: () => "Connections" },
+ handle: { crumb: () => "Connections", title: "Internal Apps" },
children: [
{
path: "buzzpay",
@@ -356,7 +368,7 @@ const routes = [
{
path: "appstore",
element: ,
- handle: { crumb: () => "App Store" },
+ handle: { crumb: () => "App Store", title: "App Store" },
children: [
{
path: ":appStoreId",
@@ -367,7 +379,7 @@ const routes = [
{
path: "channels",
element: ,
- handle: { crumb: () => "Node" },
+ handle: { crumb: () => "Node", title: "Channels" },
children: [
{
index: true,
@@ -375,7 +387,10 @@ const routes = [
},
{
path: "first",
- handle: { crumb: () => "Your First Channel" },
+ handle: {
+ crumb: () => "Your First Channel",
+ title: "Your First Channel",
+ },
children: [
{
index: true,
@@ -393,7 +408,7 @@ const routes = [
},
{
path: "auto",
- handle: { crumb: () => "New Channel" },
+ handle: { crumb: () => "New Channel", title: "New Channel" },
children: [
{
index: true,
@@ -412,34 +427,43 @@ const routes = [
{
path: "outgoing",
element: ,
- handle: { crumb: () => "Open Channel with On-Chain" },
+ handle: {
+ crumb: () => "Open Channel with On-Chain",
+ title: "Open Channel with On-Chain",
+ },
},
{
path: "incoming",
element: ,
- handle: { crumb: () => "Open Channel with Lightning" },
+ handle: {
+ crumb: () => "Open Channel with Lightning",
+ title: "Open Channel with Lightning",
+ },
},
{
path: "order",
element: ,
- handle: { crumb: () => "Current Order" },
+ handle: { crumb: () => "Current Order", title: "Current Order" },
},
{
path: "onchain/buy-bitcoin",
element: ,
- handle: { crumb: () => "Buy Bitcoin" },
+ handle: { crumb: () => "Buy Bitcoin", title: "Buy Bitcoin" },
},
{
path: "onchain/deposit-bitcoin",
element: ,
- handle: { crumb: () => "Deposit Bitcoin" },
+ handle: {
+ crumb: () => "Deposit Bitcoin",
+ title: "Deposit Bitcoin",
+ },
},
],
},
{
path: "peers",
element: ,
- handle: { crumb: () => "Peers" },
+ handle: { crumb: () => "Peers", title: "Peers" },
children: [
{
index: true,
@@ -448,7 +472,7 @@ const routes = [
{
path: "new",
element: ,
- handle: { crumb: () => "Connect Peer" },
+ handle: { crumb: () => "Connect Peer", title: "Connect Peer" },
},
],
},
@@ -459,7 +483,7 @@ const routes = [
{
path: "review-earn",
element: ,
- handle: { crumb: () => "Review & Earn" },
+ handle: { crumb: () => "Review & Earn", title: "Review & Earn" },
},
],
},