diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 8d15d1f..85177f1 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,6 +1,9 @@
diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue
index b4a9b6b..8320f39 100644
--- a/frontend/src/components/AppShell.vue
+++ b/frontend/src/components/AppShell.vue
@@ -13,6 +13,7 @@ import Icon from "./Icon.vue";
import ImportNotes from "./ImportNotes.vue";
import LabelsModal from "./LabelsModal.vue";
import { isDesktop } from "../desktop/bridge";
+import { useLensName } from "../composables/useLensName";
import { facetsToQuery } from "../notes/facets";
import { NOTE_SWATCH_CLASSES, resolveLabelColor } from "../notes/colors";
@@ -242,34 +243,7 @@ watch(
},
);
-/**
- * What to call the lens currently in view.
- *
- * Keyed off the route name rather than each view declaring its own title, so the
- * label sits in one place and can't go missing (the board never had one)
- * or drift in styling (timeline and reminders each had their own h1).
- *
- * A label lens is named by the label itself — "Groceries" is what the user came
- * looking for; "Label" would tell them nothing they didn't already know.
- */
-const lensName = computed(() => {
- switch (route.name) {
- case "archive":
- return "Archive";
- case "trash":
- return "Trash";
- case "timeline":
- return "Timeline";
- case "reminders":
- return "Reminders";
- case "label":
- // The store may not have loaded yet on a deep link; fall back rather than
- // flashing an empty slot.
- return labels.items.find((l) => l.id === String(route.params.id))?.name ?? "Label";
- default:
- return "Notes";
- }
-});
+const lensName = useLensName();
async function signOut() {
await session.logout();
diff --git a/frontend/src/composables/useLensName.ts b/frontend/src/composables/useLensName.ts
new file mode 100644
index 0000000..3b605ab
--- /dev/null
+++ b/frontend/src/composables/useLensName.ts
@@ -0,0 +1,37 @@
+import { computed, type ComputedRef } from "vue";
+import { useRoute } from "vue-router";
+import { useLabelsStore } from "../stores/labels";
+
+/**
+ * What to call the lens currently in view.
+ *
+ * Keyed off the route name rather than each view declaring its own title, so the
+ * label sits in one place and can't go missing (the board never had one)
+ * or drift in styling (timeline and reminders each had their own h1). The shell's
+ * header and the browser tab both read it, so the two can't disagree either.
+ *
+ * A label lens is named by the label itself — "Groceries" is what the user came
+ * looking for; "Label" would tell them nothing they didn't already know.
+ */
+export function useLensName(): ComputedRef {
+ const route = useRoute();
+ const labels = useLabelsStore();
+ return computed(() => {
+ switch (route.name) {
+ case "archive":
+ return "Archive";
+ case "trash":
+ return "Trash";
+ case "timeline":
+ return "Timeline";
+ case "reminders":
+ return "Reminders";
+ case "label":
+ // The store may not have loaded yet on a deep link; fall back rather than
+ // flashing an empty slot.
+ return labels.items.find((l) => l.id === String(route.params.id))?.name ?? "Label";
+ default:
+ return "Notes";
+ }
+ });
+}
diff --git a/frontend/src/composables/usePageTitle.ts b/frontend/src/composables/usePageTitle.ts
new file mode 100644
index 0000000..ff9a5db
--- /dev/null
+++ b/frontend/src/composables/usePageTitle.ts
@@ -0,0 +1,25 @@
+import { computed, watchEffect } from "vue";
+import { useRoute } from "vue-router";
+import { useConfigStore } from "../stores/config";
+import { useLensName } from "./useLensName";
+
+/**
+ * Keep the browser tab titled " · ".
+ *
+ * index.html's static only names the app, and nothing updated it after
+ * that, so every tab read the same — or, where the browser preferred the URL, no
+ * name at all. One writer, mounted once in App.vue: a route outside the shell names
+ * itself with `meta.title`, and a board lens takes the name the shell's header
+ * already shows.
+ */
+export function usePageTitle(): void {
+ const route = useRoute();
+ const config = useConfigStore();
+ const lensName = useLensName();
+
+ const page = computed(() => (route.meta.title as string | undefined) ?? lensName.value);
+
+ watchEffect(() => {
+ document.title = `${page.value} · ${config.siteName}`;
+ });
+}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index cee5542..5cc1fca 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -31,13 +31,13 @@ const router = createRouter({
path: "/capture",
name: "capture",
component: () => import("../views/CaptureView.vue"),
- meta: { requiresAuth: true, requiresDesktop: true },
+ meta: { title: "Quick capture", requiresAuth: true, requiresDesktop: true },
},
{
path: "/settings",
name: "settings",
component: () => import("../views/SettingsView.vue"),
- meta: { requiresAuth: true, requiresAdmin: true },
+ meta: { title: "Settings", requiresAuth: true, requiresAdmin: true },
},
{
// Desktop only: connect this app to a server. Meaningless in the web build,
@@ -45,7 +45,7 @@ const router = createRouter({
path: "/sync",
name: "sync",
component: () => import("../views/SyncView.vue"),
- meta: { requiresAuth: true, requiresDesktop: true },
+ meta: { title: "Sync", requiresAuth: true, requiresDesktop: true },
},
{
// Per-user account: linked devices (native-client sync tokens). Any user.
@@ -58,19 +58,19 @@ const router = createRouter({
path: "/account",
name: "account",
component: () => import("../views/AccountView.vue"),
- meta: { requiresAuth: true, requiresServer: true },
+ meta: { title: "Linked devices", requiresAuth: true, requiresServer: true },
},
{
path: "/login",
name: "login",
component: () => import("../views/LoginView.vue"),
- meta: { guestOnly: true },
+ meta: { title: "Sign in", guestOnly: true },
},
{
path: "/register",
name: "register",
component: () => import("../views/RegisterView.vue"),
- meta: { guestOnly: true },
+ meta: { title: "Create account", guestOnly: true },
},
],
});
diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue
index 52b3871..4398cb7 100644
--- a/frontend/src/views/LoginView.vue
+++ b/frontend/src/views/LoginView.vue
@@ -44,11 +44,13 @@ async function submit() {
-
- TS
-
+
Welcome back
Sign in to your thoughts.
diff --git a/frontend/src/views/RegisterView.vue b/frontend/src/views/RegisterView.vue
index ba01056..c0624af 100644
--- a/frontend/src/views/RegisterView.vue
+++ b/frontend/src/views/RegisterView.vue
@@ -2,11 +2,13 @@
import { ref } from "vue";
import { useRouter } from "vue-router";
import { useSessionStore } from "../stores/session";
+import { useConfigStore } from "../stores/config";
import BaseInput from "../components/BaseInput.vue";
import BaseButton from "../components/BaseButton.vue";
import type { ApiError } from "../api/client";
const session = useSessionStore();
+const config = useConfigStore();
const router = useRouter();
const displayName = ref("");
@@ -37,11 +39,13 @@ async function submit() {