See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix spec §7 compliance: preserve ?days= in canonical redirects and emit <link rel="canonical">

Fix 1: redirectToCanonical now validates and preserves ?days= query param
when redirecting /profile/:handle → /profile/:handle/likes, matching the
same validate-and-strip-invalid behaviour already used by #show.

Fix 2: #show constructs a canonicalUrl from APP_URL + path + optional
?days= and passes it to the view; show.edge forwards it to layout via
component props; layout.edge already had the <link rel="canonical"> logic.

Adds 5 regression tests (107 → 112): two for ?days= preservation through
redirectToCanonical, one for invalid ?days= on that path, and two for
<link rel="canonical"> presence with and without ?days=.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+86 -3
+25 -2
apps/web/app/controllers/profile_controller.ts
··· 3 3 import { HandleResolver, InvalidHandleError } from '#services/handle_resolver' 4 4 import { ClickHouseStore } from '@skystar/clickhouse' 5 5 import User from '#models/user' 6 + import env from '#start/env' 6 7 7 8 type Kind = 'likes' | 'reposts' 8 9 ··· 17 18 // GET /profile/:handle → 301 /profile/:handle/likes 18 19 // --------------------------------------------------------------------------- 19 20 20 - async redirectToCanonical({ params, response }: HttpContext) { 21 + async redirectToCanonical({ params, request, response }: HttpContext) { 21 22 const rawHandle = params.handle as string 22 23 let canonicalHandle: string 23 24 ··· 30 31 throw err 31 32 } 32 33 33 - return response.redirect().withQs(false).status(301).toPath(`/profile/${canonicalHandle}/likes`) 34 + // Preserve ?days= if present and valid (same validation as #show) 35 + const daysRaw = request.input('days') 36 + let qs = '' 37 + if (daysRaw !== null && daysRaw !== undefined && daysRaw !== '') { 38 + const parsed = Number(daysRaw) 39 + if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 3650) { 40 + return response 41 + .status(400) 42 + .send('Invalid ?days= parameter. Must be a positive integer ≤ 3650.') 43 + } 44 + qs = `?days=${parsed}` 45 + } 46 + 47 + return response 48 + .redirect() 49 + .withQs(false) 50 + .status(301) 51 + .toPath(`/profile/${canonicalHandle}/likes${qs}`) 34 52 } 35 53 36 54 // --------------------------------------------------------------------------- ··· 133 151 })) 134 152 135 153 // 6. Render profile page with Cache-Control 154 + const appUrl = env.get('APP_URL') 155 + const daysQs = daysWindow ? `?days=${daysWindow}` : '' 156 + const canonicalUrl = `${appUrl}/profile/${canonicalHandle}/${kind}${daysQs}` 157 + 136 158 response.header('Cache-Control', 'public, max-age=60') 137 159 return view.render('pages/profile/show', { 138 160 user, ··· 140 162 kind, 141 163 daysWindow, 142 164 posts: postsWithUrl, 165 + canonicalUrl, 143 166 }) 144 167 } 145 168 }
+1 -1
apps/web/resources/views/pages/profile/show.edge
··· 1 - @component('components/layout') 1 + @component('components/layout', { canonicalUrl }) 2 2 @slot('title') 3 3 Top {{ kind === 'likes' ? 'liked' : 'reposted' }} posts of @{{ handle }} — skystar 4 4 @endslot
+60
apps/web/tests/functional/profile_controller.spec.ts
··· 155 155 assert.include(response.text(), 'meta http-equiv="refresh"') 156 156 assert.include(response.text(), 'Indexing') 157 157 }) 158 + 159 + // Test 17: GET /profile/:handle preserves ?days= when redirecting to canonical /likes 160 + test('GET /profile/:handle preserves ?days= when redirecting to /likes', async ({ client }) => { 161 + const response = await client.get('/profile/dril?days=30').redirects(0) 162 + response.assertStatus(301) 163 + response.assertHeader('location', '/profile/dril.bsky.social/likes?days=30') 164 + }) 165 + 166 + // Test 18: GET /profile/:handle preserves ?days= through case-canonicalization redirect 167 + test('GET /profile/Dril.bsky.social/likes preserves ?days= through lowercasing redirect', async ({ 168 + client, 169 + }) => { 170 + const response = await client.get('/profile/Dril.bsky.social/likes?days=30').redirects(0) 171 + response.assertStatus(301) 172 + response.assertHeader('location', '/profile/dril.bsky.social/likes?days=30') 173 + }) 174 + 175 + // Test 19: ?days= invalid value returns 400 on /profile/:handle redirect 176 + test('GET /profile/:handle?days=abc returns 400 on redirectToCanonical', async ({ client }) => { 177 + const response = await client.get('/profile/dril?days=abc').redirects(0) 178 + response.assertStatus(400) 179 + }) 158 180 }) 159 181 160 182 // --------------------------------------------------------------------------- ··· 315 337 // Recent post should appear, old post should not 316 338 assert.include(html, 'Recent post within window') 317 339 assert.notInclude(html, 'Old post outside window') 340 + }).skip(async () => !(await isClickHouseAvailable()), 'ClickHouse not available') 341 + 342 + // Test 20: profile page emits <link rel="canonical"> 343 + test('profile page emits <link rel="canonical">', async ({ client, assert, swap }) => { 344 + swap(ClickHouseStore, store) 345 + 346 + await User.create({ 347 + did: 'did:plc:canonical001', 348 + handle: 'dril.bsky.social', 349 + firstSeenAt: Date.now(), 350 + backfilledAt: Date.now(), 351 + }) 352 + 353 + const response = await client.get('/profile/dril.bsky.social/likes') 354 + response.assertStatus(200) 355 + assert.include( 356 + response.text(), 357 + '<link rel="canonical" href="http://localhost:3333/profile/dril.bsky.social/likes"' 358 + ) 359 + }).skip(async () => !(await isClickHouseAvailable()), 'ClickHouse not available') 360 + 361 + // Test 21: canonical URL includes ?days= when set 362 + test('canonical URL includes ?days= when set', async ({ client, assert, swap }) => { 363 + swap(ClickHouseStore, store) 364 + 365 + await User.create({ 366 + did: 'did:plc:canonical002', 367 + handle: 'dril.bsky.social', 368 + firstSeenAt: Date.now(), 369 + backfilledAt: Date.now(), 370 + }) 371 + 372 + const response = await client.get('/profile/dril.bsky.social/likes?days=30') 373 + response.assertStatus(200) 374 + assert.include( 375 + response.text(), 376 + '<link rel="canonical" href="http://localhost:3333/profile/dril.bsky.social/likes?days=30"' 377 + ) 318 378 }).skip(async () => !(await isClickHouseAvailable()), 'ClickHouse not available') 319 379 320 380 // Test 13: /reposts shows reposts kind toggle active