Add per-page titles

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 20:52:11 +02:00
co-authored by Claude Sonnet 5
parent 7daaa95ab9
commit 97bb6cc092
6 changed files with 48 additions and 11 deletions
+3
View File
@@ -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();
+18 -5
View File
@@ -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<Metadata> {
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();
+3
View File
@@ -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();
+13 -4
View File
@@ -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<Metadata> {
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();
+8 -2
View File
@@ -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`}
>
<body className="bg-background text-foreground flex min-h-full flex-col" suppressHydrationWarning>
<body
className="bg-background text-foreground flex min-h-full flex-col"
suppressHydrationWarning
>
<TooltipProvider>
{children}
<Toaster />
+3
View File
@@ -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("/");