g1t/apps/web/app/lib/renamed.server.ts
| 1 | import { redirect } from "react-router"; |
| 2 | |
| 3 | import { isValidNamespace } from "@g1t/contracts"; |
| 4 | |
| 5 | import type { Viewer } from "@g1t/contracts"; |
| 6 | |
| 7 | import { identity, repos } from "./services.server"; |
| 8 | |
| 9 | /** |
| 10 | * A workspace that was renamed keeps its old address as a redirect for |
| 11 | * `SLUG_HOLD_DAYS`. Call this where a page is about to 404 on its first |
| 12 | * path segment: if `slug` is an old name, it throws a 301 to the same |
| 13 | * address (path and query) under the current one. Otherwise it returns, and |
| 14 | * the caller 404s as before. Only the not-found path pays for the lookup. |
| 15 | */ |
| 16 | export async function redirectIfRenamed(request: Request, slug: string): Promise<void> { |
| 17 | const old = slug.toLowerCase(); |
| 18 | // Nothing that could never have been a workspace's name is looked up. |
| 19 | if (!isValidNamespace(old)) return; |
| 20 | let current: string | null = null; |
| 21 | try { |
| 22 | current = await identity.resolveSlug(old); |
| 23 | } catch { |
| 24 | // A lookup that fails leaves the page a 404, never a 500. |
| 25 | return; |
| 26 | } |
| 27 | if (!current || current === old) return; |
| 28 | const url = new URL(request.url); |
| 29 | const segments = url.pathname.split("/"); |
| 30 | // segments[0] is the empty string before the leading slash. |
| 31 | segments[1] = current; |
| 32 | throw redirect(segments.join("/") + url.search, 301); |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * A repository transferred to another workspace keeps its old address as a |
| 37 | * redirect until a repository is made there. Call this where a project page |
| 38 | * is about to 404: if `owner/repo` is a path a repository left, and the |
| 39 | * viewer may see it where it is now, it throws a 301 to the same page |
| 40 | * there. A private repository's new address is never shown to someone who |
| 41 | * cannot see it; they get the 404 as before. |
| 42 | */ |
| 43 | export async function redirectIfTransferred( |
| 44 | request: Request, |
| 45 | owner: string, |
| 46 | repo: string, |
| 47 | viewer: Viewer, |
| 48 | ): Promise<void> { |
| 49 | let now: { namespace: string; name: string } | null = null; |
| 50 | try { |
| 51 | now = await repos.resolvePath({ namespace: owner, name: repo }); |
| 52 | if (!now || !(await repos.get(now, viewer)).ok) return; |
| 53 | } catch { |
| 54 | return; |
| 55 | } |
| 56 | const url = new URL(request.url); |
| 57 | const segments = url.pathname.split("/"); |
| 58 | segments[1] = now.namespace; |
| 59 | segments[2] = now.name; |
| 60 | throw redirect(segments.join("/") + url.search, 301); |
| 61 | } |