From 97bb6cc09208b0aeb7ff9beb3051b4e0bc41b27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 15 Aug 2026 20:52:11 +0200 Subject: [PATCH] Add per-page titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root layout now defines a title template (%s · TriggerShell) so every page just sets its own short title instead of repeating the brand name. Static titles for the dashboard, login, and run history pages; dynamic generateMetadata for the script and run detail pages, since those need the script/run name which isn't known until the route params resolve. Co-Authored-By: Claude Sonnet 5 --- app/src/app/(app)/page.tsx | 3 +++ app/src/app/(app)/runs/[runId]/page.tsx | 23 +++++++++++++++---- app/src/app/(app)/runs/page.tsx | 3 +++ app/src/app/(app)/scripts/[scriptId]/page.tsx | 17 ++++++++++---- app/src/app/layout.tsx | 10 ++++++-- app/src/app/login/page.tsx | 3 +++ 6 files changed, 48 insertions(+), 11 deletions(-) diff --git a/app/src/app/(app)/page.tsx b/app/src/app/(app)/page.tsx index 75b9afe..7cb55f3 100644 --- a/app/src/app/(app)/page.tsx +++ b/app/src/app/(app)/page.tsx @@ -1,10 +1,13 @@ export const dynamic = "force-dynamic"; +import type { Metadata } from "next"; import Link from "next/link"; import { ChevronRight, PlayCircle } from "lucide-react"; import { getConfig } from "@/lib/config/load"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +export const metadata: Metadata = { title: "Scripts" }; + export default function DashboardPage() { const { config } = getConfig(); diff --git a/app/src/app/(app)/runs/[runId]/page.tsx b/app/src/app/(app)/runs/[runId]/page.tsx index aa8cd44..5500455 100644 --- a/app/src/app/(app)/runs/[runId]/page.tsx +++ b/app/src/app/(app)/runs/[runId]/page.tsx @@ -1,6 +1,7 @@ export const dynamic = "force-dynamic"; import fs from "node:fs"; +import type { Metadata } from "next"; import { eq } from "drizzle-orm"; import { notFound } from "next/navigation"; import { getDb } from "@/lib/db/client"; @@ -10,6 +11,22 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; const MAX_INITIAL_LOG_BYTES = 200_000; +interface RunDetailPageProps { + params: Promise<{ runId: string }>; +} + +export async function generateMetadata({ + params, +}: RunDetailPageProps): Promise { + const { runId } = await params; + const run = getDb() + .select({ scriptName: runs.scriptName }) + .from(runs) + .where(eq(runs.id, runId)) + .get(); + return { title: run ? `${run.scriptName} run` : "Run not found" }; +} + function readTail(logFilePath: string): string { if (!fs.existsSync(logFilePath)) return ""; const { size } = fs.statSync(logFilePath); @@ -24,11 +41,7 @@ function readTail(logFilePath: string): string { } } -export default async function RunDetailPage({ - params, -}: { - params: Promise<{ runId: string }>; -}) { +export default async function RunDetailPage({ params }: RunDetailPageProps) { const { runId } = await params; const db = getDb(); const run = db.select().from(runs).where(eq(runs.id, runId)).get(); diff --git a/app/src/app/(app)/runs/page.tsx b/app/src/app/(app)/runs/page.tsx index c666ef7..1bd31fd 100644 --- a/app/src/app/(app)/runs/page.tsx +++ b/app/src/app/(app)/runs/page.tsx @@ -1,5 +1,6 @@ export const dynamic = "force-dynamic"; +import type { Metadata } from "next"; import Link from "next/link"; import { desc } from "drizzle-orm"; import { getDb } from "@/lib/db/client"; @@ -14,6 +15,8 @@ import { TableRow, } from "@/components/ui/table"; +export const metadata: Metadata = { title: "Run History" }; + function formatDuration(startedAt: Date | null, endedAt: Date | null): string { if (!startedAt) return "-"; const end = endedAt ?? new Date(); diff --git a/app/src/app/(app)/scripts/[scriptId]/page.tsx b/app/src/app/(app)/scripts/[scriptId]/page.tsx index ed345ba..d8a3ea5 100644 --- a/app/src/app/(app)/scripts/[scriptId]/page.tsx +++ b/app/src/app/(app)/scripts/[scriptId]/page.tsx @@ -1,5 +1,6 @@ export const dynamic = "force-dynamic"; +import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { getScript } from "@/lib/config/load"; import { serializeScriptForClient } from "@/lib/config/serialize"; @@ -12,11 +13,19 @@ import { CardTitle, } from "@/components/ui/card"; -export default async function ScriptPage({ - params, -}: { +interface ScriptPageProps { params: Promise<{ scriptId: string }>; -}) { +} + +export async function generateMetadata({ + params, +}: ScriptPageProps): Promise { + const { scriptId } = await params; + const script = getScript(scriptId); + return { title: script?.name ?? "Script not found" }; +} + +export default async function ScriptPage({ params }: ScriptPageProps) { const { scriptId } = await params; const script = getScript(scriptId); if (!script) notFound(); diff --git a/app/src/app/layout.tsx b/app/src/app/layout.tsx index 2acfa20..5b89b27 100644 --- a/app/src/app/layout.tsx +++ b/app/src/app/layout.tsx @@ -15,7 +15,10 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "TriggerShell", + title: { + default: "TriggerShell", + template: "%s · TriggerShell", + }, description: "Run configured shell scripts from a web UI.", }; @@ -29,7 +32,10 @@ export default function RootLayout({ lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > - + {children} diff --git a/app/src/app/login/page.tsx b/app/src/app/login/page.tsx index 888a61e..b39710a 100644 --- a/app/src/app/login/page.tsx +++ b/app/src/app/login/page.tsx @@ -1,10 +1,13 @@ export const dynamic = "force-dynamic"; +import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { getConfig } from "@/lib/config/load"; import { getSession } from "@/lib/auth/session"; import { LoginForm } from "./login-form"; +export const metadata: Metadata = { title: "Sign in" }; + export default async function LoginPage() { const { config } = getConfig(); if (!config.auth.enabled) redirect("/");