feat(web): add page titles, staleTime, and lift auth redirect into layout

- Add `head()` with document titles to dashboard, explore, settings, setup, login, register, and person detail routes
- Return `personName` from the person detail loader so the title can be set server-side instead of after data loads
- Add `staleTime` to all loader routes (30s for dashboard/settings, 60s for explore/titles/people)
- Move the "redirect if already authenticated" `beforeLoad` guard from individual login/register routes into the shared `_auth` layout so it runs once for all unauthenticated routes
- Add a Cloud deploy section to the README with a one-click Railway button
This commit is contained in:
2026-03-19 16:47:23 -04:00
parent e7da1e5109
commit 1670c6f494
11 changed files with 51 additions and 14 deletions
+2
View File
@@ -12,6 +12,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { orpc } from "@/lib/orpc/client";
export const Route = createFileRoute("/_app/dashboard")({
staleTime: 30_000,
head: () => ({ meta: [{ title: "Dashboard — Sofa" }] }),
loader: async ({ context }) => {
await Promise.all([
context.queryClient.ensureQueryData(orpc.dashboard.stats.queryOptions()),
+2
View File
@@ -11,6 +11,8 @@ import { Skeleton } from "@/components/ui/skeleton";
import { orpc } from "@/lib/orpc/client";
export const Route = createFileRoute("/_app/explore")({
staleTime: 60_000,
head: () => ({ meta: [{ title: "Explore — Sofa" }] }),
loader: async ({ context }) => {
await Promise.all([
context.queryClient.ensureQueryData(
+6 -4
View File
@@ -5,8 +5,9 @@ import { PersonDetailClient, PersonDetailSkeleton } from "@/components/people/pe
import { orpc } from "@/lib/orpc/client";
export const Route = createFileRoute("/_app/people/$id")({
staleTime: 60_000,
loader: async ({ params, context }) => {
await context.queryClient.ensureInfiniteQueryData(
const data = await context.queryClient.ensureInfiniteQueryData(
orpc.people.detail.infiniteOptions({
input: (pageParam: number) => ({
id: params.id,
@@ -18,10 +19,11 @@ export const Route = createFileRoute("/_app/people/$id")({
lastPage.page < lastPage.totalPages ? lastPage.page + 1 : undefined,
}),
);
return { personName: data.pages[0]?.person.name };
},
head: ({ loaderData: _loaderData, params: _params }) => {
// Title is set by the component after data loads
return {};
head: ({ loaderData }) => {
if (!loaderData?.personName) return {};
return { meta: [{ title: `${loaderData.personName} — Sofa` }] };
},
pendingComponent: () => <PersonDetailSkeleton />,
notFoundComponent: PersonNotFound,
+2
View File
@@ -28,6 +28,8 @@ import { orpc } from "@/lib/orpc/client";
const GITHUB_REPO = "jakejarvis/sofa";
export const Route = createFileRoute("/_app/settings")({
staleTime: 30_000,
head: () => ({ meta: [{ title: "Settings — Sofa" }] }),
loader: async ({ context }) => {
const promises: Promise<unknown>[] = [
context.queryClient.ensureQueryData(orpc.integrations.list.queryOptions()),
+1
View File
@@ -15,6 +15,7 @@ import { orpc } from "@/lib/orpc/client";
import { getThemeCssProperties } from "@/lib/theme";
export const Route = createFileRoute("/_app/titles/$id")({
staleTime: 60_000,
loader: async ({ params, context }) => {
const [titleResult, userInfo] = await Promise.all([
context.queryClient.ensureQueryData(
+7 -1
View File
@@ -1,6 +1,12 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { createFileRoute, redirect, Outlet } from "@tanstack/react-router";
import { authClient } from "@/lib/auth/client";
export const Route = createFileRoute("/_auth")({
beforeLoad: async () => {
const { data: session } = await authClient.getSession();
if (session) throw redirect({ to: "/dashboard" });
},
component: AuthLayout,
});
+1 -4
View File
@@ -1,19 +1,16 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { AuthForm } from "@/components/auth-form";
import { authClient } from "@/lib/auth/client";
import { client } from "@/lib/orpc/client";
export const Route = createFileRoute("/_auth/login")({
beforeLoad: async () => {
const { data: session } = await authClient.getSession();
if (session) throw redirect({ to: "/dashboard" });
const authConfig = await client.system.authConfig({});
if (authConfig.userCount === 0) throw redirect({ to: "/register" });
return { authConfig };
},
head: () => ({ meta: [{ title: "Sign in — Sofa" }] }),
component: LoginPage,
});
+2 -5
View File
@@ -1,19 +1,16 @@
import { Trans } from "@lingui/react/macro";
import { IconLock } from "@tabler/icons-react";
import { createFileRoute, Link, redirect } from "@tanstack/react-router";
import { createFileRoute, Link } from "@tanstack/react-router";
import { AuthForm } from "@/components/auth-form";
import { authClient } from "@/lib/auth/client";
import { client } from "@/lib/orpc/client";
export const Route = createFileRoute("/_auth/register")({
beforeLoad: async () => {
const { data: session } = await authClient.getSession();
if (session) throw redirect({ to: "/dashboard" });
const authConfig = await client.system.authConfig({});
return { authConfig };
},
head: () => ({ meta: [{ title: "Create account — Sofa" }] }),
component: RegisterPage,
});
+1
View File
@@ -78,6 +78,7 @@ const envSnippets = [
];
export const Route = createFileRoute("/setup")({
head: () => ({ meta: [{ title: "Setup — Sofa" }] }),
beforeLoad: async () => {
const info = await client.system.publicInfo({});
if (info.tmdbConfigured) throw redirect({ to: "/" });