Skip to content

Commit d5c10ab

Browse files
committed
chore: implement 301 redirects for trakt.tv v2 paths
The legacy `trakt.tv` (v2) site has been retired, and its URLs now land on `trakt-web`. This change introduces a new server hook that catches incoming requests for known legacy paths and issues a 301 redirect to their canonical `trakt-web` equivalents. The `legacyRedirect` handler is placed early in the `hooks.server.ts` sequence to prevent unnecessary routing, authentication, or internationalization work for requests that will immediately be redirected. It handles various patterns, including: - Structural renames (e.g., `/show/:id` to `/shows/:id`) - Mapping media pages to corresponding drawer views (e.g., `/show/:id/season/:s` to `/shows/:id?view=seasons&season=S`) - Chart/collection pages (e.g., `/shows/trending` to `/discover/trending?mode=show`, `/shows/played` to `/shows`) - Media sub-routes (e.g., `/shows/:id/comments` to `/shows/:id`) - User profile sub-routes (e.g., `/users/:id/history` to `/profile/:id/history`) - Top-level pages (e.g., `/dashboard` to `/`) This ensures that old links continue to function, guiding users to the appropriate content on the new site.
1 parent 9c97029 commit d5c10ab

6 files changed

Lines changed: 508 additions & 0 deletions

File tree

projects/client/src/hooks.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { handle as handleDeployment } from '$lib/features/deployment/handle.ts';
88
import { handle as handleDevice } from '$lib/features/devices/handle.ts';
99
import { handle as handleLocale } from '$lib/features/i18n/handle.ts';
1010
import { handle as handleImage } from '$lib/features/image/handle.ts';
11+
import { handle as handleLegacyRedirect } from '$lib/features/legacy-redirects/handle.ts';
1112
import { handle as handleMobileOperatingSystem } from '$lib/features/mobile-os/handle.ts';
1213
import { handle as handleSearchConfig } from '$lib/features/search/handle.ts';
1314
import { handle as handleTheme } from '$lib/features/theme/handle.ts';
@@ -78,6 +79,8 @@ export const handle: Handle = sequence(
7879
],
7980
}),
8081
sentryHandle(),
82+
// Retire legacy trakt.tv paths with a 301 before any routing/auth/i18n work.
83+
handleLegacyRedirect,
8184
// Must run before any feature that touches `event.locals` or session
8285
// state so missing-asset requests (which carry no user context) skip the
8386
// rest of the pipeline entirely.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { resolveLegacyRedirect } from '$lib/features/legacy-redirects/resolveLegacyRedirect.ts';
2+
import type { Handle } from '@sveltejs/kit';
3+
4+
// The v2 site (trakt.tv) is retired; its paths now land on trakt-web. Any
5+
// legacy shape with a known trakt-web equivalent is 301'd here, before routing,
6+
// so no auth/i18n/data work runs for a request we're about to redirect anyway.
7+
export const handle: Handle = ({ event, resolve }) => {
8+
const target = resolveLegacyRedirect(event.url.pathname);
9+
10+
if (!target || target === event.url.pathname) {
11+
return resolve(event);
12+
}
13+
14+
const search = event.url.search;
15+
// Merge the incoming query string, accounting for targets (e.g. the episode
16+
// drawer) that already carry their own params.
17+
const location = search && target.includes('?')
18+
? `${target}&${search.slice(1)}`
19+
: `${target}${search}`;
20+
21+
return new Response(null, {
22+
status: 301,
23+
headers: { location },
24+
});
25+
};
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { resolveLegacyRedirect } from './resolveLegacyRedirect.ts';
3+
4+
describe('util: resolveLegacyRedirect', () => {
5+
describe('media structural rename', () => {
6+
it('should map singular show to plural', () => {
7+
expect(resolveLegacyRedirect('/show/breaking-bad')).toBe(
8+
'/shows/breaking-bad',
9+
);
10+
});
11+
12+
it('should map singular show season to the seasons drawer', () => {
13+
expect(resolveLegacyRedirect('/show/breaking-bad/season/2')).toBe(
14+
'/shows/breaking-bad?view=seasons&season=2',
15+
);
16+
});
17+
18+
it('should map singular show episode to the episode drawer', () => {
19+
expect(
20+
resolveLegacyRedirect('/show/breaking-bad/season/2/episode/5'),
21+
).toBe('/shows/breaking-bad?view=episode&season=2&episode=5');
22+
});
23+
24+
it('should map singular movie to plural', () => {
25+
expect(resolveLegacyRedirect('/movie/heretic-2024')).toBe(
26+
'/movies/heretic-2024',
27+
);
28+
});
29+
30+
it('should map singular user to plural', () => {
31+
expect(resolveLegacyRedirect('/user/sean')).toBe('/users/sean');
32+
});
33+
});
34+
35+
describe('chart / collection pages', () => {
36+
it('should map discover-backed show charts to discover with mode', () => {
37+
expect(resolveLegacyRedirect('/shows/trending')).toBe(
38+
'/discover/trending?mode=show',
39+
);
40+
expect(resolveLegacyRedirect('/shows/popular')).toBe(
41+
'/discover/popular?mode=show',
42+
);
43+
expect(resolveLegacyRedirect('/shows/anticipated')).toBe(
44+
'/discover/anticipated?mode=show',
45+
);
46+
});
47+
48+
it('should map discover-backed movie charts to discover with mode', () => {
49+
expect(resolveLegacyRedirect('/movies/trending')).toBe(
50+
'/discover/trending?mode=movie',
51+
);
52+
expect(resolveLegacyRedirect('/movies/releases')).toBe(
53+
'/discover/releases?mode=movie',
54+
);
55+
});
56+
57+
it('should fold charts without a drilldown to discover with mode', () => {
58+
expect(resolveLegacyRedirect('/shows/played')).toBe(
59+
'/discover?mode=show',
60+
);
61+
expect(resolveLegacyRedirect('/shows/library')).toBe(
62+
'/discover?mode=show',
63+
);
64+
expect(resolveLegacyRedirect('/movies/boxoffice')).toBe(
65+
'/discover?mode=movie',
66+
);
67+
});
68+
69+
it('should map the bare media index to discover with mode', () => {
70+
expect(resolveLegacyRedirect('/shows')).toBe('/discover?mode=show');
71+
expect(resolveLegacyRedirect('/movies')).toBe('/discover?mode=movie');
72+
});
73+
});
74+
75+
describe('media sub-routes fold to nearest parent', () => {
76+
it('should fold show sub-routes to the show page', () => {
77+
expect(resolveLegacyRedirect('/shows/breaking-bad/comments')).toBe(
78+
'/shows/breaking-bad',
79+
);
80+
expect(resolveLegacyRedirect('/shows/breaking-bad/credits')).toBe(
81+
'/shows/breaking-bad',
82+
);
83+
});
84+
85+
it('should fold seasons/all to the seasons drawer on the first season', () => {
86+
expect(resolveLegacyRedirect('/shows/breaking-bad/seasons/all')).toBe(
87+
'/shows/breaking-bad?view=seasons&season=1',
88+
);
89+
});
90+
91+
it('should fold season sub-routes to the season drawer', () => {
92+
expect(
93+
resolveLegacyRedirect('/shows/breaking-bad/seasons/2/comments'),
94+
).toBe('/shows/breaking-bad?view=seasons&season=2');
95+
});
96+
97+
it('should fold episode sub-routes to the episode drawer', () => {
98+
expect(
99+
resolveLegacyRedirect(
100+
'/shows/breaking-bad/seasons/2/episodes/5/comments',
101+
),
102+
).toBe('/shows/breaking-bad?view=episode&season=2&episode=5');
103+
});
104+
105+
it('should fold movie sub-routes to the movie page', () => {
106+
expect(resolveLegacyRedirect('/movies/heretic-2024/releases')).toBe(
107+
'/movies/heretic-2024',
108+
);
109+
});
110+
111+
it('should fold person lists to the person page', () => {
112+
expect(resolveLegacyRedirect('/people/bryan-cranston/lists')).toBe(
113+
'/people/bryan-cranston',
114+
);
115+
});
116+
});
117+
118+
describe('user profile sub-routes', () => {
119+
it('should map history to the profile history page', () => {
120+
expect(resolveLegacyRedirect('/users/sean/history')).toBe(
121+
'/profile/sean/history',
122+
);
123+
});
124+
125+
it('should map favorites to the profile favorites page', () => {
126+
expect(resolveLegacyRedirect('/users/sean/favorites')).toBe(
127+
'/profile/sean/favorites',
128+
);
129+
});
130+
131+
it('should map ratings and comments to the profile activity drawer', () => {
132+
expect(resolveLegacyRedirect('/users/sean/ratings')).toBe(
133+
'/profile/sean?view=activity',
134+
);
135+
expect(resolveLegacyRedirect('/users/sean/comments/movies')).toBe(
136+
'/profile/sean?view=activity',
137+
);
138+
});
139+
140+
it('should map hidden to the signed-in user media progress page', () => {
141+
expect(resolveLegacyRedirect('/users/sean/hidden')).toBe(
142+
'/profile/me/progress?mode=media',
143+
);
144+
});
145+
146+
it('should fold remaining profile sub-routes to the profile home', () => {
147+
expect(resolveLegacyRedirect('/users/sean/likes')).toBe('/profile/sean');
148+
expect(resolveLegacyRedirect('/users/sean/network')).toBe(
149+
'/profile/sean',
150+
);
151+
});
152+
153+
it('should fold numeric-id user lists to the lists overview', () => {
154+
expect(resolveLegacyRedirect('/users/sean/lists/12345')).toBe(
155+
'/users/sean/lists',
156+
);
157+
});
158+
});
159+
160+
describe('top-level pages', () => {
161+
it('should map dashboard to home', () => {
162+
expect(resolveLegacyRedirect('/dashboard')).toBe('/');
163+
expect(resolveLegacyRedirect('/dashboard/on_deck')).toBe('/');
164+
});
165+
166+
it('should map official lists', () => {
167+
expect(resolveLegacyRedirect('/officiallist/best-of-2024')).toBe(
168+
'/lists/official/best-of-2024',
169+
);
170+
});
171+
});
172+
173+
describe('no sensible target falls back to home', () => {
174+
it.each([
175+
'/seasons/12345',
176+
'/episodes/67890',
177+
'/comments/all/movies',
178+
'/share/abc123',
179+
'/tmdb/updates',
180+
])('should redirect %s to home', (path) => {
181+
expect(resolveLegacyRedirect(path)).toBe('/');
182+
});
183+
});
184+
185+
describe('numeric-id lists fall back to the user lists landing', () => {
186+
it.each([
187+
'/lists/12345',
188+
'/watchlist/12345',
189+
])('should redirect %s to /users/me/lists', (path) => {
190+
expect(resolveLegacyRedirect(path)).toBe('/users/me/lists');
191+
});
192+
});
193+
194+
describe('native paths pass through untouched', () => {
195+
it.each([
196+
'/shows/breaking-bad',
197+
'/shows/breaking-bad/lists',
198+
'/shows/breaking-bad/related',
199+
'/shows/breaking-bad/seasons',
200+
'/shows/breaking-bad/seasons/2',
201+
'/shows/breaking-bad/seasons/2/episodes/5',
202+
'/movies/heretic-2024',
203+
'/movies/heretic-2024/lists',
204+
'/people/bryan-cranston',
205+
'/users/sean',
206+
'/users/sean/lists',
207+
'/users/sean/watchlist',
208+
'/users/sean/progress',
209+
'/users/sean/collection',
210+
'/calendar',
211+
'/search',
212+
'/settings/advanced',
213+
'/',
214+
])('should return null for %s', (path) => {
215+
expect(resolveLegacyRedirect(path)).toBeNull();
216+
});
217+
});
218+
});

0 commit comments

Comments
 (0)