feat: dynamic tab titles based on route Uses /state + in layout to set document.title per route (browse, upload, settings, login, image/#id). Also updates title in image viewer overlay when opening/closing/navigating between images.
Maxwell Jensen maxwelljensen@posteo.net
Thu, 14 May 2026 18:09:14 +0200
3 files changed,
38 insertions(+),
1 deletions(-)
M
web/src/lib/ImageViewer.svelte
→
web/src/lib/ImageViewer.svelte
@@ -87,6 +87,8 @@ function prev() {
if (currentIndex > 0) { currentIndex--; loadImage(); + history.replaceState(null, '', '/image/' + current.id); + document.title = document.title.replace(/ - Image #\d+$/, '') + ' - Image #' + current.id; } }@@ -94,6 +96,8 @@ function next() {
if (currentIndex < images.length - 1) { currentIndex++; loadImage(); + history.replaceState(null, '', '/image/' + current.id); + document.title = document.title.replace(/ - Image #\d+$/, '') + ' - Image #' + current.id; } }
M
web/src/routes/+layout.svelte
→
web/src/routes/+layout.svelte
@@ -1,5 +1,6 @@
<script lang="ts"> import { onMount } from 'svelte'; + import { page } from '$app/state'; import { logout, getMe, getConfig, type CurrentUser, type SiteConfig } from '$lib/api'; import Avatar from '$lib/Avatar.svelte';@@ -8,15 +9,44 @@
let user = $state<CurrentUser | null>(null); let checking = $state(true); let siteConfig = $state<SiteConfig | null>(null); + let baseTitle = $state('weimar'); onMount(() => { getConfig() - .then((c) => { siteConfig = c; document.title = c.site_title; }) + .then((c) => { + siteConfig = c; + baseTitle = c.site_title; + }) .catch(() => {}); getMe() .then((u) => { user = u; }) .catch(() => {}) .finally(() => { checking = false; }); + }); + + $effect(() => { + const path = page.url.pathname; + const base = baseTitle; + switch (path) { + case '/browse': + document.title = base + ' - Browse'; + break; + case '/upload': + document.title = base + ' - Upload'; + break; + case '/settings': + document.title = base + ' - Settings'; + break; + case '/login': + document.title = base + ' - Login'; + break; + default: + if (path.startsWith('/image/')) { + document.title = base + ' - Image #' + path.split('/')[2]; + } else { + document.title = base; + } + } }); async function handleLogout() {
M
web/src/routes/browse/+page.svelte
→
web/src/routes/browse/+page.svelte
@@ -145,6 +145,7 @@
function openViewer(index: number) { viewerIndex = index; history.replaceState(null, '', '/image/' + images[index].id); + document.title = document.title.replace(/ - .*$/, '') + ' - Image #' + images[index].id; } function closeViewer() {@@ -152,7 +153,9 @@ viewerIndex = -1;
const params = new URLSearchParams(); if (filterTag) params.set('tag', filterTag); if (filterUploader) params.set('uploader', filterUploader); + const baseTitle = document.title.replace(/ - .*$/, ''); history.replaceState(null, '', params.toString() ? '/browse?' + params.toString() : '/browse'); + document.title = baseTitle + ' - Browse'; // Restore main layout style. const mainEl = document.querySelector('.main') as HTMLElement; if (mainEl) {