diff --git a/.server-changes/page-tab-titles.md b/.server-changes/page-tab-titles.md new file mode 100644 index 00000000000..78cb5b53bc7 --- /dev/null +++ b/.server-changes/page-tab-titles.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: improvement +--- + +Browser tabs now name the page you're on, and the run, task or queue when you're looking at one, instead of all reading "Trigger.dev". diff --git a/apps/webapp/app/root.tsx b/apps/webapp/app/root.tsx index d7ad416af24..4e4fd1a90db 100644 --- a/apps/webapp/app/root.tsx +++ b/apps/webapp/app/root.tsx @@ -30,7 +30,7 @@ import { } from "~/utils/themePreference"; import { cachedFlag } from "~/v3/featureFlags.server"; import { getTimezonePreference } from "./services/preferences/uiPreferences.server"; -import { appEnvTitleTag } from "./utils"; +import { appTitle } from "./utils/pageTitle"; // Derived here (not inside StaleAssetRecovery) so the shared component takes // the flag as a prop. NODE_ENV is statically replaced in browser bundles, and @@ -51,7 +51,8 @@ export const headers = () => ({ export const meta: MetaFunction = ({ data }) => { const typedData = data as UseDataFunctionReturn; return [ - { title: typedData?.appEnv ? `Trigger.dev${appEnvTitleTag(typedData.appEnv)}` : "Trigger.dev" }, + // Pages declare their own title with `pageMeta`; this is the fallback. + { title: appTitle(typedData?.appEnv) }, { name: "viewport", content: "width=1024, initial-scale=1", diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx index b6fe0b09c68..5d016e96bc2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.invite/route.tsx @@ -38,6 +38,9 @@ import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashb import { acceptInvitePath, organizationTeamPath, v3BillingPath } from "~/utils/pathBuilder"; import { isAtOrBelow } from "~/utils/inviteRoleLadder"; import { PurchaseSeatsModal } from "../_app.orgs.$organizationSlug.settings.team/route"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Invite team members"); const Params = z.object({ organizationSlug: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx index 5a30d931c52..d49d5c83398 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx @@ -1,5 +1,5 @@ import { BookOpenIcon, ExclamationTriangleIcon } from "@heroicons/react/20/solid"; -import { json, type MetaFunction } from "@remix-run/node"; +import { json } from "@remix-run/node"; import { useFetcher, useRevalidator } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import type { TaskRunStatus } from "@trigger.dev/database"; @@ -100,10 +100,9 @@ import { v3TasksStreamingPath, v3TestTaskPath, } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: `Tasks | Trigger.dev` }]; -}; +export const meta = pageMeta("Tasks"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx index 5d9a86e4a25..04cc057b773 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx @@ -1,5 +1,4 @@ import { BookOpenIcon } from "@heroicons/react/24/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense, useMemo, useState } from "react"; import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; @@ -50,11 +49,12 @@ import { v3PlaygroundAgentPath, } from "~/utils/pathBuilder"; import { parseFiniteInt } from "~/utils/searchParams"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = ({ data }) => { - const slug = (data as { agent?: AgentDetail | null } | undefined)?.agent?.slug; - return [{ title: slug ? `${slug} | Agents | Trigger.dev` : "Agent | Trigger.dev" }]; -}; +export const meta = pageMeta(({ data, params }) => [ + data?.agent?.slug ?? params.agentParam ?? "Agent", + "Agents", +]); const AgentParamSchema = EnvironmentParamSchema.extend({ agentParam: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx index f121900e901..e0ee26255b7 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx @@ -46,6 +46,9 @@ import { assertSafeWebhookUrl, UnsafeWebhookUrlError, } from "~/v3/services/alerts/safeWebhookUrl.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("New alert"); const FormSchema = z .object({ diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx index 62ecec6890f..bb53dd94dc8 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx @@ -10,7 +10,7 @@ import { PlusIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, Outlet, useActionData, useNavigation } from "@remix-run/react"; +import { Form, Outlet, useActionData, useNavigation } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { SlackIcon } from "@trigger.dev/companyicons"; import type { ProjectAlertChannelType, ProjectAlertType } from "@trigger.dev/database"; @@ -66,14 +66,9 @@ import { v3ProjectAlertsPath, } from "~/utils/pathBuilder"; import { alertsWorker } from "~/v3/alertsWorker.server"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Alerts | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Alerts"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx index cb6df265573..7702c3b1b77 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx @@ -1,5 +1,4 @@ import { BookOpenIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { CopyableText } from "~/components/primitives/CopyableText"; @@ -38,14 +37,9 @@ import { ApiKeysPresenter } from "~/presenters/v3/ApiKeysPresenter.server"; import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { cn } from "~/utils/cn"; import { docsPath, EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `API keys | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("API keys"); export const loader = dashboardLoader( { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx index 37224720713..756b4cfcce4 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx @@ -24,6 +24,9 @@ import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { formatNumber } from "~/utils/numberFormatter"; import { EnvironmentParamSchema, v3BatchesPath, v3BatchRunsPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ params }) => [params.batchParam ?? "Batch", "Batches"]); const BatchParamSchema = EnvironmentParamSchema.extend({ batchParam: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index 9543e80243b..4507f528ad3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -1,6 +1,6 @@ import { ExclamationCircleIcon } from "@heroicons/react/20/solid"; import { BookOpenIcon } from "@heroicons/react/24/solid"; -import { type MetaFunction, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react"; +import { Outlet, useLocation, useNavigation, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -62,14 +62,9 @@ import { v3BatchRunsPath, } from "~/utils/pathBuilder"; import { throwNotFound } from "~/utils/httpErrors"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Batches | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Batches"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx index 909a8bb6381..22c9dc7035a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx @@ -79,6 +79,9 @@ import { ArchiveButton } from "../resources.branches.archive"; import { NewBranchPanel } from "~/routes/resources.branches.create"; import { BranchesOptions } from "~/utils/branches"; import { IconArrowBearRight2 } from "@tabler/icons-react"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Preview branches"); const PurchaseSchema = z.discriminatedUnion("action", [ z.object({ diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx index 15728fec69f..2a4816e5a65 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx @@ -42,6 +42,12 @@ import { v3RunsPath, } from "~/utils/pathBuilder"; import { BulkActionService } from "~/v3/services/bulk/BulkActionV2.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ data, params }) => [ + data?.bulkAction?.name || params.bulkActionParam || "Bulk action", + "Bulk actions", +]); const BulkActionParamSchema = EnvironmentParamSchema.extend({ bulkActionParam: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx index 1fa646dac6f..6dbc22bb868 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx @@ -1,6 +1,6 @@ import { BookOpenIcon, PlusIcon } from "@heroicons/react/20/solid"; import { NoSymbolIcon } from "@heroicons/react/24/solid"; -import { Outlet, useParams, type MetaFunction } from "@remix-run/react"; +import { Outlet, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -54,14 +54,9 @@ import { v3BulkActionPath, v3CreateBulkActionPath, } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Bulk actions | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Bulk actions"); const SearchParamsSchema = z.object({ page: z.coerce.number().optional(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx index e9542b5c8f5..18dc4b3409b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx @@ -7,13 +7,7 @@ import { InformationCircleIcon, } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { - Form, - useActionData, - useNavigation, - useSearchParams, - type MetaFunction, -} from "@remix-run/react"; +import { Form, useActionData, useNavigation, useSearchParams } from "@remix-run/react"; import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { useEffect, useState } from "react"; @@ -74,14 +68,9 @@ import { concurrencyPath, EnvironmentParamSchema, v3BillingPath } from "~/utils/ import { AllocateConcurrencyService } from "~/v3/services/allocateConcurrency.server"; import { SetConcurrencyAddOnService } from "~/v3/services/setConcurrencyAddOn.server"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Manage concurrency | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Manage concurrency"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx index 170a0f49a46..91004be8280 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route.tsx @@ -42,6 +42,12 @@ import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server"; import { QueryScopeSchema } from "~/v3/querySchemas"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { MetricWidget } from "../resources.metric"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ data }) => [ + data?.title ?? "Dashboard", + "Dashboards", +]); const ParamSchema = EnvironmentParamSchema.extend({ dashboardKey: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx index 1cee0dde8e2..bad9010a1e8 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards._index/route.tsx @@ -1,5 +1,5 @@ import { PlusIcon } from "@heroicons/react/20/solid"; -import { Link, type MetaFunction } from "@remix-run/react"; +import { Link } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -18,10 +18,9 @@ import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { EnvironmentParamSchema, v3BuiltInDashboardPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: "Dashboards | Trigger.dev" }]; -}; +export const meta = pageMeta("Dashboards"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx index 6f7c1d2609c..0a1b448564f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.custom.$dashboardId/route.tsx @@ -58,6 +58,12 @@ import { } from "~/utils/pathBuilder"; import { MetricDashboard } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardKey/route"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ data }) => [ + data?.title ?? "Dashboard", + "Dashboards", +]); const ParamSchema = EnvironmentParamSchema.extend({ dashboardId: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx index c4950277288..73e1752715b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx @@ -52,6 +52,12 @@ import { v3DeploymentParams, v3DeploymentsPath, v3RunsPath } from "~/utils/pathB import { capitalizeWord } from "~/utils/string"; import { UserTag } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route"; import { DeploymentEventFromString } from "@trigger.dev/core/v3/schemas"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ params }) => [ + params.deploymentParam ?? "Deployment", + "Deployments", +]); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx index b9a50851832..822050f19f9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx @@ -4,15 +4,7 @@ import { BookOpenIcon, NoSymbolIcon, } from "@heroicons/react/20/solid"; -import { - Form, - type MetaFunction, - Outlet, - useLocation, - useNavigate, - useNavigation, - useParams, -} from "@remix-run/react"; +import { Form, Outlet, useLocation, useNavigate, useNavigation, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { CogIcon, GitBranchIcon } from "lucide-react"; import { useEffect } from "react"; @@ -87,14 +79,9 @@ import { compareDeploymentVersions } from "~/v3/utils/deploymentVersions"; import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; import { env } from "~/env.server"; import { DialogClose } from "@radix-ui/react-dialog"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Deployments | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Deployments"); const SearchParams = z.object({ page: z.coerce.number().optional(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev-branches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev-branches/route.tsx index dd2679085e4..8580cecead9 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev-branches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dev-branches/route.tsx @@ -46,6 +46,9 @@ import { NewBranchPanel } from "~/routes/resources.branches.create"; import { BranchesOptions } from "~/utils/branches"; import { IconArrowBearRight2 } from "@tabler/icons-react"; import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Dev branches"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx index 02d6ef00c4d..6904815fc5e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx @@ -56,6 +56,9 @@ import { import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; import { EnvironmentVariableKey } from "~/v3/environmentVariables/repository"; import { findUnauthorizedEnvironmentId } from "~/v3/writableEnvironments"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("New environment variable"); const Variable = z.object({ key: EnvironmentVariableKey, diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx index bd152e58033..e783cbbdfac 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx @@ -16,7 +16,6 @@ import { useFetcher, useNavigation, useRevalidator, - type MetaFunction, } from "@remix-run/react"; import { json } from "@remix-run/server-runtime"; import { useVirtualizer } from "@tanstack/react-virtual"; @@ -88,14 +87,9 @@ import { shouldSyncEnvVar, type TriggerEnvironmentType, } from "~/v3/vercel/vercelProjectIntegrationSchema"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Environment variables | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Environment variables"); type PageVercelIntegration = NonNullable< Awaited>["vercelIntegration"] diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx index 6ec1aa7fce1..dbf3233c234 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx @@ -1,6 +1,6 @@ import { parseWithZod } from "@conform-to/zod"; import { BellAlertIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction, useFetcher, useRevalidator } from "@remix-run/react"; +import { useFetcher, useRevalidator } from "@remix-run/react"; import { type ActionFunctionArgs, json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { IconAlarmSnooze as IconAlarmSnoozeBase, IconCircleDotted } from "@tabler/icons-react"; import { ErrorId } from "@trigger.dev/core/v3/isomorphic"; @@ -83,14 +83,12 @@ import { } from "~/utils/pathBuilder"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { ErrorGroupActions } from "~/v3/services/errorGroupActions.server"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = ({ data }) => { - return [ - { - title: `Error Details | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta(({ params }) => [ + params.fingerprint ? ErrorId.toFriendlyId(params.fingerprint) : "Error", + "Errors", +]); const emptyStringToUndefined = z.preprocess( (v) => (v === "" ? undefined : v), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx index 5243ab3d46b..4aed2249520 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx @@ -1,6 +1,6 @@ import * as Ariakit from "@ariakit/react"; import { BellAlertIcon, XMarkIcon } from "@heroicons/react/20/solid"; -import { Form, useFetcher, useRevalidator, type MetaFunction } from "@remix-run/react"; +import { Form, useFetcher, useRevalidator } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { ErrorId } from "@trigger.dev/core/v3/isomorphic"; import { type ErrorGroupStatus } from "@trigger.dev/database"; @@ -76,14 +76,9 @@ import { requireUser } from "~/services/session.server"; import { formatNumberCompact } from "~/utils/numberFormatter"; import { EnvironmentParamSchema, v3ErrorPath } from "~/utils/pathBuilder"; import { ServiceValidationError } from "~/v3/services/baseService.server"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Errors | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Errors"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx index 40eb8356f73..833dffdec64 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx @@ -1,5 +1,4 @@ import { CheckIcon, BookOpenIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { IconCardsFilled, IconDiamondFilled, IconTallymark4 } from "@tabler/icons-react"; import { tryCatch } from "@trigger.dev/core"; @@ -48,14 +47,9 @@ import { EnvironmentParamSchema, organizationBillingPath, } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Limits | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Limits"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx index a963fc02100..78ab68276fb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx @@ -1,5 +1,5 @@ import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; -import { type MetaFunction, useFetcher, useNavigation, useLocation, Form } from "@remix-run/react"; +import { useFetcher, useNavigation, useLocation, Form } from "@remix-run/react"; import { XMarkIcon } from "@heroicons/react/20/solid"; import { ServiceValidationError } from "~/v3/services/baseService.server"; import { @@ -42,6 +42,7 @@ import { } from "~/components/primitives/Resizable"; import { Button } from "~/components/primitives/Buttons"; import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags"; +import { pageMeta } from "~/utils/pageTitle"; // Valid log levels for filtering const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]; @@ -52,13 +53,7 @@ function parseLevelsFromUrl(url: URL): LogLevel[] | undefined { return levelParams.filter((l): l is LogLevel => validLevels.includes(l as LogLevel)); } -export const meta: MetaFunction = () => { - return [ - { - title: `Logs | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Logs"); // TODO: Move this to a more appropriate shared location async function hasLogsPageAccess( diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx index e27f63a7e29..6ea0713dfbb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.$modelId/route.tsx @@ -1,5 +1,4 @@ import { ArrowsRightLeftIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -33,14 +32,16 @@ import { formatTokenCount, } from "~/utils/modelFormatters"; import { EnvironmentParamSchema, v3ModelComparePath, v3ModelsPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; const ParamSchema = EnvironmentParamSchema.extend({ modelId: z.string(), }); -export const meta: MetaFunction = () => { - return [{ title: "Model Detail | Trigger.dev" }]; -}; +export const meta = pageMeta(({ data, params }) => [ + data?.model?.modelName ?? params.modelId ?? "Model", + "Models", +]); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx index 744c29235aa..24f08ee6954 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models._index/route.tsx @@ -6,12 +6,7 @@ import { CubeIcon, XMarkIcon, } from "@heroicons/react/20/solid"; -import { - Form, - type MetaFunction, - type ShouldRevalidateFunctionArgs, - useFetcher, -} from "@remix-run/react"; +import { Form, type ShouldRevalidateFunctionArgs, useFetcher } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { AnimatePresence, motion } from "framer-motion"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -110,10 +105,9 @@ import { parseFiniteInt } from "~/utils/searchParams"; import { IconColumns3 } from "@tabler/icons-react"; import { type loader as compareLoader } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: "Models | Trigger.dev" }]; -}; +export const meta = pageMeta("Models"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route.tsx index 41f1d430092..c286a7f4661 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.models.compare/route.tsx @@ -1,5 +1,4 @@ import { ArrowsRightLeftIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; @@ -28,10 +27,9 @@ import { useEnvironment } from "~/hooks/useEnvironment"; import { EnvironmentParamSchema, v3ModelsPath } from "~/utils/pathBuilder"; import { formatModelCost } from "~/utils/modelFormatters"; import { formatNumberCompact } from "~/utils/numberFormatter"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: "Compare Models | Trigger.dev" }]; -}; +export const meta = pageMeta("Compare Models"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx index 581c52c2114..64692acb58a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.$agentParam/route.tsx @@ -2,7 +2,6 @@ import type { UIMessage } from "@ai-sdk/react"; import { useChat } from "@ai-sdk/react"; import { BoltIcon, CheckIcon, StopIcon } from "@heroicons/react/20/solid"; import { ClipboardIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { type MetaFunction } from "@remix-run/node"; import { Link, useFetcher, useNavigate, useRouteLoaderData } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { generateJWT as internal_generateJWT, MachinePresetName } from "@trigger.dev/core/v3"; @@ -59,10 +58,9 @@ import { extractJwtSigningSecretKey } from "~/services/realtime/jwtAuth.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: "Playground | Trigger.dev" }]; -}; +export const meta = pageMeta(({ params }) => [params.agentParam ?? "Agent", "Playground"]); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground/route.tsx index ddb9f2a63eb..e45e7218f48 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground/route.tsx @@ -1,5 +1,5 @@ import { BookOpenIcon, ChevronUpDownIcon, CpuChipIcon } from "@heroicons/react/20/solid"; -import { json, type MetaFunction } from "@remix-run/node"; +import { json } from "@remix-run/node"; import { Outlet, useLoaderData, useNavigate, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { CubeSparkleIcon } from "~/assets/icons/CubeSparkleIcon"; @@ -24,10 +24,9 @@ import { playgroundPresenter } from "~/presenters/v3/PlaygroundPresenter.server" import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server"; import { requireUser } from "~/services/session.server"; import { docsPath, EnvironmentParamSchema, v3PlaygroundAgentPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: "Playground | Trigger.dev" }]; -}; +export const meta = pageMeta("Playground"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx index cfdfb44fec7..96a86ba3008 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts.$promptSlug/route.tsx @@ -1,7 +1,7 @@ import * as Ariakit from "@ariakit/react"; import { ArrowPathIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { type MetaFunction, useFetcher } from "@remix-run/react"; +import { useFetcher } from "@remix-run/react"; import { json, type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; import { AnimatePresence, motion } from "framer-motion"; @@ -85,14 +85,16 @@ import { MetricWidget } from "~/routes/resources.metric"; import { cn } from "~/utils/cn"; import { EnvironmentParamSchema, v3PromptsPath, v3RunSpanPath } from "~/utils/pathBuilder"; import { parsePeriodToMs } from "~/utils/periods"; +import { pageMeta } from "~/utils/pageTitle"; const ParamSchema = EnvironmentParamSchema.extend({ promptSlug: z.string(), }); -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${(data as any)?.prompt.slug ?? "Prompt"} | Trigger.dev` }]; -}; +export const meta = pageMeta(({ data, params }) => [ + data?.prompt?.slug ?? params.promptSlug ?? "Prompt", + "Prompts", +]); // ─── Action ────────────────────────────────────────────── diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsx index 651ce47c780..f8725bc4b3e 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.prompts._index/route.tsx @@ -1,4 +1,3 @@ -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Bar, @@ -39,10 +38,9 @@ import { requireUserId } from "~/services/session.server"; import { docsPath, EnvironmentParamSchema, v3PromptsPath } from "~/utils/pathBuilder"; import { LinkButton } from "~/components/primitives/Buttons"; import { BookOpenIcon } from "@heroicons/react/24/solid"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: "Prompts | Trigger.dev" }]; -}; +export const meta = pageMeta("Prompts"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx index 4f5a2c1e52d..402345aaa09 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx @@ -18,6 +18,9 @@ import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useEnvironment } from "~/hooks/useEnvironment"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Query"); /** Convert a Date or ISO string to ISO string format */ function toISOString(value: Date | string): string { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx index d128f53a894..0b15bf66c0b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx @@ -7,7 +7,7 @@ import { RectangleStackIcon, } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { Form, useNavigation, type MetaFunction } from "@remix-run/react"; +import { Form, useNavigation } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import type { RuntimeEnvironmentType } from "@trigger.dev/database"; import { useEffect, useMemo, useState, type ReactNode } from "react"; @@ -112,6 +112,7 @@ import { useRememberQueueMetricsPeriod, } from "~/components/queues/queueMetricsPeriod"; import { queueMetricsMaxPeriodDays } from "~/components/queues/queueMetricsPeriod.server"; +import { pageMeta } from "~/utils/pageTitle"; const SearchParamsSchema = z.object({ query: z.string().optional(), @@ -134,13 +135,7 @@ const QUEUE_LIVE_BLOCKS_QUERY = // inspector). const LIVE_GAUGE_FRESH_MS = 90_000; -export const meta: MetaFunction = () => { - return [ - { - title: `Queues | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Queues"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx index 55494258f79..866add305ad 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues_.$queueParam/route.tsx @@ -1,4 +1,3 @@ -import { type MetaFunction } from "@remix-run/react"; import { type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import type { QueueItem } from "@trigger.dev/core/v3/schemas"; @@ -79,8 +78,12 @@ import { Paragraph } from "~/components/primitives/Paragraph"; import { InlineCode } from "~/components/code/InlineCode"; import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon"; import { BookOpenIcon } from "@heroicons/react/20/solid"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => [{ title: `Queue metrics | Trigger.dev` }]; +export const meta = pageMeta(({ data, params }) => [ + data?.queue?.name ?? params.queueParam ?? "Queue", + "Queues", +]); const ParamsSchema = EnvironmentParamSchema.extend({ queueParam: z.string() }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx index c4d28ded12a..d1a64ad1d58 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx @@ -64,6 +64,9 @@ import { v3BillingPath, } from "~/utils/pathBuilder"; import { SetDefaultRegionService } from "~/v3/services/setDefaultRegion.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Regions"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 085fa8f23ac..6c8a955f9d5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -121,6 +121,9 @@ import { import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ params }) => [params.runParam ?? "Run", "Runs"]); const resizableSettings = { parent: { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index dfb921e2691..8cbd02a9100 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -1,5 +1,5 @@ import { BeakerIcon, BookOpenIcon } from "@heroicons/react/24/solid"; -import { type MetaFunction, useLocation, useNavigation, useRevalidator } from "@remix-run/react"; +import { useLocation, useNavigation, useRevalidator } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense, useState } from "react"; import { @@ -76,16 +76,11 @@ import { shouldRevalidateRunsList, } from "./shouldRevalidateRunsList"; import { useRunsLiveReload } from "./useRunsLiveReload"; +import { pageMeta } from "~/utils/pageTitle"; export { shouldRevalidateRunsList as shouldRevalidate }; -export const meta: MetaFunction = () => { - return [ - { - title: `Runs metrics | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Runs"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx index a3a0ef05470..529bec93aed 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx @@ -17,6 +17,9 @@ import { requireUserId } from "~/services/session.server"; import { v3EnvironmentPath, v3ScheduleParams, v3SchedulePath } from "~/utils/pathBuilder"; import { DeleteTaskScheduleService } from "~/v3/services/deleteTaskSchedule.server"; import { SetActiveOnTaskScheduleService } from "~/v3/services/setActiveOnTaskSchedule.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ params }) => [params.scheduleParam ?? "Schedule", "Schedules"]); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx index b01b9a7d93e..645fcb1c5fb 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.edit.$scheduleParam/route.tsx @@ -5,6 +5,9 @@ import { requireUserId } from "~/services/session.server"; import { v3EnvironmentPath, v3ScheduleParams } from "~/utils/pathBuilder"; import { humanToCronSupported } from "~/v3/humanToCron.server"; import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Edit schedule"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx index b254988e7d9..272b87b3053 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx @@ -5,6 +5,9 @@ import { requireUserId } from "~/services/session.server"; import { EnvironmentParamSchema } from "~/utils/pathBuilder"; import { humanToCronSupported } from "~/v3/humanToCron.server"; import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("New schedule"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx index 19773257d2e..1de8d7fcbfa 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx @@ -1,6 +1,5 @@ import { BoltIcon, BoltSlashIcon } from "@heroicons/react/20/solid"; import { BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { useVirtualizer } from "@tanstack/react-virtual"; import { Clipboard, ClipboardCheck } from "lucide-react"; @@ -72,14 +71,13 @@ import { v3RunsPath, v3SessionsPath, } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; const ParamsSchema = EnvironmentParamSchema.extend({ sessionParam: z.string(), }); -export const meta: MetaFunction = () => { - return [{ title: `Session | Trigger.dev` }]; -}; +export const meta = pageMeta(({ params }) => [params.sessionParam ?? "Session", "Sessions"]); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx index 2f05fb2e834..0bb25715dbe 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx @@ -1,5 +1,4 @@ import { BookOpenIcon } from "@heroicons/react/24/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { QuestionMarkIcon } from "~/assets/icons/QuestionMarkIcon"; @@ -24,14 +23,9 @@ import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstan import { requireUserId } from "~/services/session.server"; import { docsPath, EnvironmentParamSchema } from "~/utils/pathBuilder"; import { throwNotFound } from "~/utils/httpErrors"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Sessions | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Sessions"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx index 9bc54b19bf1..3aca7f06ac2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings.integrations/route.tsx @@ -40,6 +40,9 @@ import { VercelOnboardingModal, VercelSettingsPanel, } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Integrations"); export const handle = { pageTitle: "Integrations" }; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx index 65d7ab14278..7ce5cbd2dd5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx @@ -1,4 +1,4 @@ -import { Outlet, useMatches, type MetaFunction } from "@remix-run/react"; +import { Outlet, useMatches } from "@remix-run/react"; import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; @@ -12,14 +12,9 @@ import { v3ProjectSettingsGeneralPath, v3ProjectSettingsIntegrationsPath, } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Project settings | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Project settings"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx index e6fcb09bbc6..fc514043208 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.dashboard/route.tsx @@ -1,5 +1,4 @@ import { BookOpenIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense, useMemo } from "react"; import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; @@ -28,10 +27,9 @@ import { v3EnvironmentPath, v3RunsPath, } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: "Tasks | Trigger.dev" }]; -}; +export const meta = pageMeta("Tasks"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx index b3a2b852ed9..3191cd4f8c5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx @@ -1,5 +1,5 @@ import { BookOpenIcon, PlusIcon } from "@heroicons/react/20/solid"; -import { useFetcher, useRevalidator, type MetaFunction } from "@remix-run/react"; +import { useFetcher, useRevalidator } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { TypedAwait, typeddefer, useTypedFetcher, useTypedLoaderData } from "remix-typedjson"; @@ -105,13 +105,12 @@ import type { loader as scheduleNewLoader } from "../_app.orgs.$organizationSlug import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; import { UpsertScheduleForm } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route"; import { NewRunsButton, TaskRunsList } from "~/components/runs/v3/TaskRunsList"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = ({ data }) => { - const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug; - return [ - { title: slug ? `${slug} | Scheduled tasks | Trigger.dev` : "Scheduled task | Trigger.dev" }, - ]; -}; +export const meta = pageMeta(({ data, params }) => [ + data?.task?.slug ?? params.taskParam ?? "Task", + "Scheduled tasks", +]); const ParamsSchema = EnvironmentParamSchema.extend({ taskParam: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx index 15df31370d2..3263febe240 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx @@ -1,4 +1,3 @@ -import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDurationMilliseconds } from "@trigger.dev/core/v3"; import { Suspense, useMemo, useRef, useState } from "react"; @@ -65,11 +64,12 @@ import { parseFiniteInt } from "~/utils/searchParams"; import { NewRunsButton, TaskRunsList } from "~/components/runs/v3/TaskRunsList"; import { canAccessQueueMetricsUi } from "~/v3/canAccessQueueMetricsUi.server"; import { engine } from "~/v3/runEngine.server"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = ({ data }) => { - const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug; - return [{ title: slug ? `${slug} | Tasks | Trigger.dev` : "Task | Trigger.dev" }]; -}; +export const meta = pageMeta(({ data, params }) => [ + data?.task?.slug ?? params.taskParam ?? "Task", + "Tasks", +]); const ParamsSchema = EnvironmentParamSchema.extend({ taskParam: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx index ffe6dbcd038..eca0f7686cd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx @@ -80,6 +80,9 @@ import { AIPayloadTabContent } from "./AIPayloadTabContent"; import { SchemaTabContent } from "./SchemaTabContent"; import { TestSidebarTabs } from "./TestSidebarTabs"; import { Header2 } from "~/components/primitives/Headers"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ params }) => [params.taskParam ?? "Task", "Test"]); type FormAction = "create-template" | "delete-template" | "run-scheduled" | "run-standard"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx index 2c29521fef8..f3a39546a92 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx @@ -1,5 +1,5 @@ import { BookOpenIcon, MagnifyingGlassIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction, Outlet, useParams } from "@remix-run/react"; +import { Outlet, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { TestHasNoTasks } from "~/components/BlankStatePanels"; @@ -36,14 +36,9 @@ import { type TaskListItem, TestPresenter } from "~/presenters/v3/TestPresenter. import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { docsPath, EnvironmentParamSchema, v3TestTaskPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Test | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Test"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx index d584d5f970f..879e815567a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx @@ -25,6 +25,12 @@ import { WaitpointDetailTable } from "~/components/runs/v3/WaitpointDetails"; import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable"; import { InfoIconTooltip } from "~/components/primitives/Tooltip"; import { logger } from "~/services/logger.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta(({ params }) => [ + params.waitpointParam ?? "Waitpoint", + "Waitpoint tokens", +]); const Params = EnvironmentParamSchema.extend({ waitpointParam: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx index 011ad8a3680..ae80228e3e5 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx @@ -1,5 +1,5 @@ import { BookOpenIcon } from "@heroicons/react/20/solid"; -import { Outlet, useParams, type MetaFunction } from "@remix-run/react"; +import { Outlet, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; @@ -49,14 +49,9 @@ import { type PrismaClientOrTransaction, } from "~/db.server"; import { docsPath, EnvironmentParamSchema, v3WaitpointTokenPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Waitpoint tokens | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Waitpoint tokens"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx index 348658ba9ef..5830f599a6a 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx @@ -7,7 +7,7 @@ import { FolderIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { Form, type MetaFunction, useActionData, useNavigation, useSubmit } from "@remix-run/react"; +import { Form, useActionData, useNavigation, useSubmit } from "@remix-run/react"; import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { useEffect, useRef, useState } from "react"; import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -54,14 +54,9 @@ import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder"; import { cn } from "~/utils/cn"; import { extractDomain, faviconUrl as buildFaviconUrl } from "~/utils/favicon"; import { OrganizationParamsSchema, organizationSettingsPath, rootPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Organization settings | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Organization settings"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx index a7790c4ee48..3dc613bdeda 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-limits/route.tsx @@ -1,5 +1,4 @@ import { parseWithZod } from "@conform-to/zod"; -import type { MetaFunction } from "@remix-run/react"; import { json, redirect } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -74,15 +73,14 @@ import { v3BillingLimitsPath, v3BillingPath, } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; const billingLimitsAuthorization = { action: "manage" as const, resource: { type: "billing-limits" as const }, }; -export const meta: MetaFunction = () => { - return [{ title: `Billing limits | Trigger.dev` }]; -}; +export const meta = pageMeta("Billing limits"); export const loader = dashboardLoader( { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx index 28737236bc5..2c98feadf6c 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx @@ -1,5 +1,4 @@ import { CalendarDaysIcon, CreditCardIcon, StarIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { type PlanDefinition } from "@trigger.dev/platform"; import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson"; import { Feedback } from "~/components/Feedback"; @@ -21,14 +20,9 @@ import { v3StripePortalPath, } from "~/utils/pathBuilder"; import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Billing | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Billing"); export const loader = dashboardLoader( { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.slack.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.slack.tsx index a7950a41e39..ca51044da84 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.slack.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.slack.tsx @@ -40,6 +40,9 @@ import { $transaction, prisma } from "~/db.server"; import { requireOrganization } from "~/services/org.server"; import { OrganizationParamsSchema, organizationSlackIntegrationPath } from "~/utils/pathBuilder"; import { logger } from "~/services/logger.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Slack integration"); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const { organizationSlug } = OrganizationParamsSchema.parse(params); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.vercel.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.vercel.tsx index a8412a0765b..15241df2fb3 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.vercel.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.integrations.vercel.tsx @@ -35,6 +35,9 @@ import { requireOrganization } from "~/services/org.server"; import { rbac } from "~/services/rbac.server"; import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder"; import { OrganizationParamsSchema, v3ProjectSettingsIntegrationsPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Vercel integration"); function formatDate(date: Date): string { return new Intl.DateTimeFormat("en-US", { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx index 914e9ceb232..cb2480c1707 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx @@ -4,7 +4,7 @@ import { PlusIcon, TrashIcon, } from "@heroicons/react/20/solid"; -import { Form, useRevalidator, type MetaFunction } from "@remix-run/react"; +import { Form, useRevalidator } from "@remix-run/react"; import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core/utils"; import type { PrivateLinkConnectionStatus } from "@trigger.dev/platform"; @@ -33,10 +33,9 @@ import { } from "~/utils/pathBuilder"; import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: `Private Connections | Trigger.dev` }]; -}; +export const meta = pageMeta("Private Connections"); export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx index 1dc9753ac13..23355edb0fd 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx @@ -1,6 +1,6 @@ import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; -import { Form, useActionData, useParams, type MetaFunction } from "@remix-run/react"; +import { Form, useActionData, useParams } from "@remix-run/react"; import { json, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core/utils"; import { useState } from "react"; @@ -45,10 +45,9 @@ import { SparklesIcon, TrashIcon, } from "@heroicons/react/20/solid"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [{ title: `Add Private Connection | Trigger.dev` }]; -}; +export const meta = pageMeta("Add Private Connection"); export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.roles/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.roles/route.tsx index d334fb50fec..d8e39bcaddf 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.roles/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.roles/route.tsx @@ -1,5 +1,4 @@ import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { useState } from "react"; import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; @@ -27,14 +26,9 @@ import { useShowSelfServe } from "~/hooks/useShowSelfServe"; import { resolveOrgIdFromSlug } from "~/models/organization.server"; import { rbac } from "~/services/rbac.server"; import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Roles | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Roles"); const Params = z.object({ organizationSlug: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx index e24fceb332c..84bf36e37c2 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.sso/route.tsx @@ -4,7 +4,6 @@ import { ClockIcon, ExclamationCircleIcon, } from "@heroicons/react/20/solid"; -import { type MetaFunction } from "@remix-run/react"; import { redirect } from "@remix-run/server-runtime"; import { useEffect, useState } from "react"; import { useFetcher, useRevalidator } from "@remix-run/react"; @@ -48,8 +47,9 @@ import { FEATURE_FLAG } from "~/v3/featureFlags"; import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { cn } from "~/utils/cn"; import { throwPermissionDenied } from "~/utils/permissionDenied"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => [{ title: "SSO & Directory Sync | Trigger.dev" }]; +export const meta = pageMeta("SSO & Directory Sync"); const Params = z.object({ organizationSlug: z.string() }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx index cc2a95571d0..868985b2057 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx @@ -2,13 +2,7 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { EnvelopeIcon, NoSymbolIcon, UserPlusIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { - Form, - type MetaFunction, - useActionData, - useFetcher, - useNavigation, -} from "@remix-run/react"; +import { Form, useActionData, useFetcher, useNavigation } from "@remix-run/react"; import { json } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core/utils"; import { cloneElement, useEffect, useRef, useState } from "react"; @@ -61,14 +55,9 @@ import { } from "~/utils/pathBuilder"; import { SetSeatsAddOnService } from "~/v3/services/setSeatsAddOn.server"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Team | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Team"); const Params = z.object({ organizationSlug: z.string(), diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx index 03636f285d2..05e7fef8860 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx @@ -1,5 +1,5 @@ import { InformationCircleIcon } from "@heroicons/react/20/solid"; -import { Await, type MetaFunction } from "@remix-run/react"; +import { Await } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDurationMilliseconds } from "@trigger.dev/core/v3"; import { Suspense, useMemo } from "react"; @@ -36,14 +36,9 @@ import { formatCurrency, formatCurrencyAccurate, formatNumber } from "~/utils/nu import { useBillingLimit } from "~/hooks/useOrganizations"; import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder"; import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Usage | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Usage"); export async function loader({ params, request }: LoaderFunctionArgs) { const userId = await requireUserId(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx index 70c07a6dfcb..6f44617d358 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx @@ -36,6 +36,9 @@ import { v3ProjectPath, } from "~/utils/pathBuilder"; import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("New project"); const WORKING_ON_OTHER = "Other/not sure yet"; const GOALS_OTHER = "Other/not sure yet"; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx index a01b0793619..29839ecc100 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug_.select-plan/route.tsx @@ -9,6 +9,9 @@ import { getCurrentPlan, getPlans } from "~/services/platform.v3.server"; import { dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { OrganizationParamsSchema, organizationPath } from "~/utils/pathBuilder"; import { PricingPlans } from "../resources.orgs.$organizationSlug.select-plan"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Choose a plan"); export const loader = dashboardLoader( { diff --git a/apps/webapp/app/routes/_app.orgs.new/route.tsx b/apps/webapp/app/routes/_app.orgs.new/route.tsx index b06d9bafd93..f87d3981596 100644 --- a/apps/webapp/app/routes/_app.orgs.new/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.new/route.tsx @@ -27,6 +27,9 @@ import { NewOrganizationPresenter } from "~/presenters/NewOrganizationPresenter. import { requireUser, requireUserId } from "~/services/session.server"; import { extractDomain, faviconUrl } from "~/utils/favicon"; import { organizationPath, rootPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("New organization"); const schema = z.object({ orgName: z.string().min(3).max(50), diff --git a/apps/webapp/app/routes/_app.timezones/route.tsx b/apps/webapp/app/routes/_app.timezones/route.tsx index 39e3b7b6e8a..702c4487c80 100644 --- a/apps/webapp/app/routes/_app.timezones/route.tsx +++ b/apps/webapp/app/routes/_app.timezones/route.tsx @@ -5,6 +5,9 @@ import { LogoIcon } from "~/components/LogoIcon"; import { Header1 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import { getTimezones } from "~/utils/timezones.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Supported timezones"); export const loader = async ({ request }: LoaderFunctionArgs) => { return typedjson({ diff --git a/apps/webapp/app/routes/account._index/route.tsx b/apps/webapp/app/routes/account._index/route.tsx index 4ecccb33d68..39dbf4b4e6e 100644 --- a/apps/webapp/app/routes/account._index/route.tsx +++ b/apps/webapp/app/routes/account._index/route.tsx @@ -2,13 +2,7 @@ import { getFormProps, getInputProps, useForm } from "@conform-to/react"; import { useEffect, useState } from "react"; import { conformZodMessage, parseWithZod } from "@conform-to/zod"; import { ComputerDesktopIcon, MoonIcon, SunIcon, SwatchIcon } from "@heroicons/react/20/solid"; -import { - Form, - type MetaFunction, - useActionData, - useFetcher, - useLoaderData, -} from "@remix-run/react"; +import { Form, useActionData, useFetcher, useLoaderData } from "@remix-run/react"; import { type ActionFunction, json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { z } from "zod"; import { UserProfilePhoto } from "~/components/UserProfilePhoto"; @@ -44,6 +38,7 @@ import { cachedFlag } from "~/v3/featureFlags.server"; import { requireUser, requireUserId } from "~/services/session.server"; import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation"; import { accountPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; const THEME_LABELS: Record = { classic: "Classic", @@ -75,13 +70,7 @@ function themeIcon(value: ThemePreference) { } } -export const meta: MetaFunction = () => { - return [ - { - title: `Your profile | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Your profile"); function createSchema( constraints: { diff --git a/apps/webapp/app/routes/account.authorization-code.$authorizationCode/route.tsx b/apps/webapp/app/routes/account.authorization-code.$authorizationCode/route.tsx index 067f6af4465..32eb853b71e 100644 --- a/apps/webapp/app/routes/account.authorization-code.$authorizationCode/route.tsx +++ b/apps/webapp/app/routes/account.authorization-code.$authorizationCode/route.tsx @@ -15,6 +15,9 @@ import { isAuthorizationCodeMintable, } from "~/services/personalAccessToken.server"; import { requireUserId } from "~/services/session.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Authorize login"); const ParamsSchema = z.object({ authorizationCode: z.string(), diff --git a/apps/webapp/app/routes/account.security/route.tsx b/apps/webapp/app/routes/account.security/route.tsx index 4dc6fa76f73..62f08f6e08b 100644 --- a/apps/webapp/app/routes/account.security/route.tsx +++ b/apps/webapp/app/routes/account.security/route.tsx @@ -1,4 +1,3 @@ -import { type MetaFunction } from "@remix-run/react"; import type { LoaderFunctionArgs } from "@remix-run/server-runtime"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { @@ -16,14 +15,9 @@ import { } from "~/services/sessionDuration.server"; import { MfaSetup } from "../resources.account.mfa.setup/route"; import { SessionDurationSetting } from "../resources.account.session-duration/SessionDurationSetting"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Security | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Security"); export async function loader({ request }: LoaderFunctionArgs) { const user = await requireUser(request); diff --git a/apps/webapp/app/routes/account.tokens/route.tsx b/apps/webapp/app/routes/account.tokens/route.tsx index 1179d070ead..0b33a2a0468 100644 --- a/apps/webapp/app/routes/account.tokens/route.tsx +++ b/apps/webapp/app/routes/account.tokens/route.tsx @@ -3,7 +3,7 @@ import { parseWithZod } from "@conform-to/zod"; import { BookOpenIcon, ShieldCheckIcon, TrashIcon } from "@heroicons/react/20/solid"; import { ShieldExclamationIcon } from "@heroicons/react/24/solid"; import { DialogClose } from "@radix-ui/react-dialog"; -import { Form, type MetaFunction, useActionData, useFetcher } from "@remix-run/react"; +import { Form, useActionData, useFetcher } from "@remix-run/react"; import { type ActionFunction, type LoaderFunctionArgs, json } from "@remix-run/server-runtime"; import { useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; @@ -47,14 +47,9 @@ import { } from "~/services/personalAccessToken.server"; import { requireUserId } from "~/services/session.server"; import { docsPath, personalAccessTokensPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; -export const meta: MetaFunction = () => { - return [ - { - title: `Personal Access Tokens | Trigger.dev`, - }, - ]; -}; +export const meta = pageMeta("Personal Access Tokens"); // Shared between the create-token panel hint and the listing column // header tooltip so the cap is explained identically in both places. diff --git a/apps/webapp/app/routes/confirm-basic-details.tsx b/apps/webapp/app/routes/confirm-basic-details.tsx index 4e370913240..6af3da7bd7b 100644 --- a/apps/webapp/app/routes/confirm-basic-details.tsx +++ b/apps/webapp/app/routes/confirm-basic-details.tsx @@ -30,6 +30,9 @@ import { requireUserId } from "~/services/session.server"; import { emailSchema, MAX_EMAIL_LENGTH } from "~/utils/emailValidation"; import { rootPath } from "~/utils/pathBuilder"; import { getVercelInstallParams } from "~/v3/vercel"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Confirm your details"); const referralSourceOptions = [ "Search engine", diff --git a/apps/webapp/app/routes/invites.tsx b/apps/webapp/app/routes/invites.tsx index 6cf667c8677..527a2842138 100644 --- a/apps/webapp/app/routes/invites.tsx +++ b/apps/webapp/app/routes/invites.tsx @@ -25,6 +25,9 @@ import { import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { requireUser } from "~/services/session.server"; import { invitesPath, rootPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Your invitations"); export const loader = async ({ request }: LoaderFunctionArgs) => { const user = await requireUser(request); diff --git a/apps/webapp/app/routes/unsubscribe.$userId.$token.tsx b/apps/webapp/app/routes/unsubscribe.$userId.$token.tsx index 076a6155a40..e342171b681 100644 --- a/apps/webapp/app/routes/unsubscribe.$userId.$token.tsx +++ b/apps/webapp/app/routes/unsubscribe.$userId.$token.tsx @@ -11,6 +11,9 @@ import { TextLink } from "~/components/primitives/TextLink"; import { prisma } from "~/db.server"; import { env } from "~/env.server"; import { rootPath } from "~/utils/pathBuilder"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Unsubscribe"); export const ParamsSchema = z.object({ userId: z.string(), diff --git a/apps/webapp/app/routes/vercel.onboarding.tsx b/apps/webapp/app/routes/vercel.onboarding.tsx index fb4d223978c..f30f22cb81f 100644 --- a/apps/webapp/app/routes/vercel.onboarding.tsx +++ b/apps/webapp/app/routes/vercel.onboarding.tsx @@ -24,6 +24,9 @@ import { ssoRedirectForEmail } from "~/services/ssoAutoDiscovery.server"; import { confirmBasicDetailsPath, newProjectPath } from "~/utils/pathBuilder"; import { redirectWithErrorMessage } from "~/models/message.server"; import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server"; +import { pageMeta } from "~/utils/pageTitle"; + +export const meta = pageMeta("Vercel setup"); const LoaderParamsSchema = z.object({ organizationId: z.string().optional().nullable(), diff --git a/apps/webapp/app/utils/pageTitle.test.ts b/apps/webapp/app/utils/pageTitle.test.ts new file mode 100644 index 00000000000..526fbd0074a --- /dev/null +++ b/apps/webapp/app/utils/pageTitle.test.ts @@ -0,0 +1,147 @@ +import type { MetaDescriptor, MetaFunction } from "@remix-run/node"; +import { describe, expect, it } from "vitest"; +import { appTitle, pageMeta } from "./pageTitle"; + +const ORGANIZATION_MATCH_ID = "routes/_app.orgs.$organizationSlug"; + +type Route = { id: string; data?: unknown; meta?: MetaFunction }; + +/** + * Mirrors how Remix v2 resolves meta (see @remix-run/react `Meta`): each match is visited from the + * root down, a match without a meta export inherits the nearest ancestor's array, and the last + * match's array is what renders. It also only passes the matches visited *so far*, which is why + * titles are declared per route rather than centrally. + */ +/** The URL params a page inside a project carries; the org scope is dropped when they are present. */ +const ENV_PARAMS = { organizationSlug: "acme", projectParam: "my-project", envParam: "prod" }; + +function renderTitle( + routes: Route[], + params: Record = ENV_PARAMS +): string | undefined { + const matches: Array<{ id: string; data: unknown; meta: MetaDescriptor[]; params: any }> = []; + let leafMeta: MetaDescriptor[] = []; + + routes.forEach((route, index) => { + matches[index] = { id: route.id, data: route.data, meta: [], params }; + const routeMeta = route.meta + ? route.meta({ data: route.data, params, matches, location: {} as any } as any) + : [...leafMeta]; + matches[index].meta = routeMeta as MetaDescriptor[]; + leafMeta = routeMeta as MetaDescriptor[]; + }); + + const rendered = leafMeta.find((descriptor) => "title" in descriptor) as + | { title: string } + | undefined; + return rendered?.title; +} + +const rootRoute: Route = { + id: "root", + data: { appEnv: "production" }, + meta: () => [{ title: appTitle("production") }, { name: "viewport", content: "width=1024" }], +}; + +const orgRoute: Route = { + id: ORGANIZATION_MATCH_ID, + data: { + organization: { title: "Acme" }, + }, +}; + +describe("pageMeta", () => { + it("titles a page inside a project without any scope", () => { + const title = renderTitle([rootRoute, orgRoute, { id: "routes/runs", meta: pageMeta("Runs") }]); + + expect(title).toBe("Runs | Trigger.dev"); + }); + + it("uses the deepest declared title", () => { + const title = renderTitle([ + rootRoute, + orgRoute, + { id: "routes/runs", meta: pageMeta("Runs") }, + { id: "routes/runs.$runParam", meta: pageMeta(["run_abc", "Runs"]) }, + ]); + + expect(title).toBe("run_abc | Runs | Trigger.dev"); + }); + + it("falls back to the nearest declared title, then the app title", () => { + expect( + renderTitle([ + rootRoute, + orgRoute, + { id: "routes/runs", meta: pageMeta("Runs") }, + { id: "routes/runs._index" }, + ]) + ).toBe("Runs | Trigger.dev"); + + expect(renderTitle([rootRoute, orgRoute, { id: "routes/runs._index" }])).toBe("Trigger.dev"); + }); + + it("renders an entity title from loader data, with a fallback when it is missing", () => { + const meta = pageMeta<() => { task?: { slug: string } }>(({ data, params }) => [ + data?.task?.slug ?? params.taskParam ?? "Task", + "Tasks", + ]); + + expect( + renderTitle([ + rootRoute, + orgRoute, + { id: "routes/task", data: { task: { slug: "my-task" } }, meta }, + ]) + ).toBe("my-task | Tasks | Trigger.dev"); + + expect( + renderTitle([rootRoute, orgRoute, { id: "routes/task", meta }], { + ...ENV_PARAMS, + taskParam: "other", + }) + ).toBe("other | Tasks | Trigger.dev"); + }); + + it("names the organization on its own pages, with the app env tag", () => { + const stagingRoot: Route = { + id: "root", + data: { appEnv: "staging" }, + meta: () => [{ title: appTitle("staging") }], + }; + + const title = renderTitle( + [ + stagingRoot, + { id: ORGANIZATION_MATCH_ID, data: { organization: { title: "Acme" } } }, + { id: "routes/team", meta: pageMeta("Team") }, + ], + { organizationSlug: "acme" } + ); + + expect(title).toBe("Team | Acme | Trigger.dev (staging)"); + }); + + it("carries the root's non-title tags through", () => { + const routes = [rootRoute, orgRoute, { id: "routes/runs", meta: pageMeta("Runs") }]; + const descriptors = pageMeta("Runs")({ + data: undefined, + params: {}, + matches: [ + { id: "root", data: rootRoute.data, meta: [{ name: "viewport", content: "width=1024" }] }, + { id: "routes/runs", data: undefined, meta: [] }, + ], + location: {} as any, + } as any) as MetaDescriptor[]; + + expect(descriptors).toContainEqual({ name: "viewport", content: "width=1024" }); + expect(renderTitle(routes)).toBe("Runs | Trigger.dev"); + }); + + it("does not name the organization inside a project", () => { + // The dashboard switches projects in every tab at once, so the scope adds nothing there. + expect(renderTitle([rootRoute, orgRoute, { id: "routes/runs", meta: pageMeta("Runs") }])).toBe( + "Runs | Trigger.dev" + ); + }); +}); diff --git a/apps/webapp/app/utils/pageTitle.ts b/apps/webapp/app/utils/pageTitle.ts new file mode 100644 index 00000000000..3942cec7825 --- /dev/null +++ b/apps/webapp/app/utils/pageTitle.ts @@ -0,0 +1,105 @@ +import type { MetaDescriptor, MetaFunction } from "@remix-run/node"; +import type { UseDataFunctionReturn } from "remix-typedjson"; +import { appEnvTitleTag } from "~/utils"; + +/** + * Tab titles. Every page exports `export const meta = pageMeta(...)` and only says what the page + * is; the app title is added here. + * + * Shape (leading words carry the information, so a narrow tab still reads): + * env page: `Runs | Trigger.dev` + * env entity: `run_abc | Runs | Trigger.dev` + * org page: `Team | Acme | Trigger.dev` + * else: `Login to Trigger.dev` etc. + * + * Remix v2 picks the meta of the deepest route that exports one; a route without a meta export + * inherits its nearest ancestor's. So a layout's `pageMeta` is the fallback for children that + * don't declare their own, and non-title tags from the root (viewport, robots) are carried over + * here rather than re-declared per route. + */ + +const APP_NAME = "Trigger.dev"; + +/** The org route carries the organization the URL resolved to. */ +const ORGANIZATION_MATCH_ID = "routes/_app.orgs.$organizationSlug"; + +/** One or more title segments, most specific first: `["run_abc", "Runs"]`. */ +export type TitleSegments = string | string[]; + +type MetaArgs = Parameters[0]; +type Matches = MetaArgs["matches"]; + +type PageInput = + | TitleSegments + | ((args: { + data: UseDataFunctionReturn | undefined; + params: Record; + }) => TitleSegments | undefined); + +/** The title used when no page declares one. */ +export function appTitle(appEnv?: string): string { + return `${APP_NAME}${appEnvTitleTag(appEnv)}`; +} + +/** + * Declares this page's tab title. Pass a string (or segments), or a function of the route's + * loader data and params for entity pages. + */ +export function pageMeta(page: PageInput): MetaFunction { + return ({ data, params, matches }) => { + const segments = resolveSegments( + page, + data as UseDataFunctionReturn | undefined, + params + ); + return [...inheritedMeta(matches), { title: composePageTitle(segments, matches) }]; + }; +} + +/** Builds the full title from the page segments, the org scope and the app title. */ +export function composePageTitle(segments: string[], matches: Matches): string { + return [...segments, scopeFromMatches(matches), appTitle(appEnvFromMatches(matches))] + .filter((segment): segment is string => Boolean(segment)) + .join(" | "); +} + +/** + * The organization, and only on its own pages: inside a project the tab is already about one + * project, and the dashboard switches projects in every tab at once, so naming it adds nothing. + */ +export function scopeFromMatches(matches: Matches): string | undefined { + const match = matches.find((m) => m.id === ORGANIZATION_MATCH_ID); + if (!match || match.params?.projectParam) return undefined; + const data = match.data as { organization?: { title?: string | null } } | undefined; + return data?.organization?.title ?? undefined; +} + +function appEnvFromMatches(matches: Matches): string | undefined { + const rootData = matches[0]?.data as { appEnv?: string } | undefined; + return rootData?.appEnv; +} + +/** Non-title tags (viewport, robots) from the nearest ancestor, which Remix would otherwise drop. */ +function inheritedMeta(matches: Matches): MetaDescriptor[] { + const parent = matches[matches.length - 2]; + return (parent?.meta ?? []).filter((descriptor) => !("title" in descriptor)); +} + +function resolveSegments( + page: PageInput, + data: UseDataFunctionReturn | undefined, + params: Record +): string[] { + const resolved = typeof page === "function" ? safeResolve(() => page({ data, params })) : page; + if (!resolved) return []; + return (Array.isArray(resolved) ? resolved : [resolved]).filter(Boolean); +} + +// Loader data can be missing (error boundaries, redirects), so a bad lookup must not break the page. +function safeResolve(resolve: () => TitleSegments | undefined): TitleSegments | undefined { + try { + return resolve(); + } catch { + return undefined; + } +}