Skip to content

Commit d2efe29

Browse files
committed
Add /deeplink/* redirect route
Resolves the signed-in user's current organization, project and environment and redirects to the canonical page, so /deeplink/apikeys lands on /orgs/{org}/projects/{project}/env/{env}/apikeys. Only the environment page segments navigation already knows about are followed (derived from ENV_PAGE_META), so an unrecognised path redirects to the environment root rather than becoming the redirect target. Deeper segments and the query string are preserved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MvNi2jRtYatPQdXFJRK9SW
1 parent c084fa6 commit d2efe29

3 files changed

Lines changed: 68 additions & 0 deletions

File tree

.server-changes/deeplink-routes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Short links like /deeplink/apikeys now take you straight to that page in your current project and environment, so you no longer need the full URL with your org, project and environment in it.

apps/webapp/app/components/navigation/favoritePages.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,14 @@ const ENV_PAGE_META: Record<string, PageMeta> = {
237237
playground: { icon: "test", name: "Test", singular: "Test" },
238238
};
239239

240+
/**
241+
* The page segments that live under an environment ("apikeys", "runs", …), derived from the pages
242+
* navigation knows about. Used by the /deeplink/* route to validate its redirect target.
243+
*/
244+
export const ENV_PAGE_SEGMENTS: ReadonlySet<string> = new Set(
245+
Object.keys(ENV_PAGE_META).filter((segment) => segment.length > 0)
246+
);
247+
240248
const ORG_SETTINGS_PAGE_META: Record<string, PageMeta> = {
241249
"": { icon: "org-settings", name: "Organization settings" },
242250
team: { icon: "team", name: "Team" },
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { ENV_PAGE_SEGMENTS } from "~/components/navigation/favoritePages";
3+
import { prisma } from "~/db.server";
4+
import { SelectBestEnvironmentPresenter } from "~/presenters/SelectBestEnvironmentPresenter.server";
5+
import { requireUser } from "~/services/session.server";
6+
import { newOrganizationPath, newProjectPath, v3EnvironmentPath } from "~/utils/pathBuilder";
7+
8+
/**
9+
* Stable links that don't name an org, project or environment: /deeplink/apikeys redirects to
10+
* /orgs/{org}/projects/{project}/env/{env}/apikeys for whoever is signed in. Only pages the
11+
* dashboard knows about are followed, so an unrecognised path can never become the redirect
12+
* target — it lands on the resolved environment instead.
13+
*/
14+
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
15+
const user = await requireUser(request);
16+
17+
const segments = (params["*"] ?? "").split("/").filter(Boolean);
18+
//deeper segments are kept, so /deeplink/runs/run_123 reaches the run
19+
const page = ENV_PAGE_SEGMENTS.has(segments[0] ?? "") ? segments.join("/") : undefined;
20+
21+
const presenter = new SelectBestEnvironmentPresenter();
22+
try {
23+
const { project, organization, environment } = await presenter.call({ user });
24+
const environmentPath = v3EnvironmentPath(organization, project, environment);
25+
26+
if (!page) {
27+
return redirect(environmentPath);
28+
}
29+
30+
const { search } = new URL(request.url);
31+
return redirect(`${environmentPath}/${page}${search}`);
32+
} catch (_e) {
33+
//the presenter throws when the user has no projects, same as the dashboard index
34+
const organization = await prisma.organization.findFirst({
35+
where: {
36+
members: {
37+
some: {
38+
userId: user.id,
39+
},
40+
},
41+
deletedAt: null,
42+
},
43+
orderBy: {
44+
createdAt: "desc",
45+
},
46+
});
47+
48+
if (organization) {
49+
return redirect(newProjectPath(organization));
50+
}
51+
52+
return redirect(newOrganizationPath());
53+
}
54+
};

0 commit comments

Comments
 (0)