this repo has no description
1
fork

Configure Feed

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

refactor(api): change redirect shortlink from /r/ to /go/

Avoids confusion with Reddit subreddit URLs.

+24 -24
+1 -1
cmd/tumble/main.go
··· 357 357 mux.HandleFunc("/api/v1/kittens/", h.APIv1KittensDailyHandler) 358 358 359 359 // Public redirect shortlink 360 - mux.HandleFunc("/r/", h.APIv1RedirectHandler) 360 + mux.HandleFunc("/go/", h.APIv1RedirectHandler) 361 361 362 362 // Start 363 363 addr := ":" + cfg.Port
+1 -1
docs/plans/2026-02-08-api-redesign-design.md
··· 58 58 59 59 | Method | Endpoint | Description | 60 60 |--------|----------|-------------| 61 - | GET | `/r/{id}` | Redirect to link URL | 61 + | GET | `/go/{id}` | Redirect to link URL | 62 62 63 63 ## Content Negotiation 64 64
+1 -1
docs/plans/2026-02-08-api-v1-implementation.md
··· 1410 1410 1411 1411 ## Phase 8: Redirect Shortlink 1412 1412 1413 - ### Task 8.1: Implement GET /r/{id} 1413 + ### Task 8.1: Implement GET /go/{id} 1414 1414 1415 1415 **Files:** 1416 1416 - Create: `internal/handler/redirect.go`
+2 -2
internal/assets/openapi.json
··· 449 449 } 450 450 }, 451 451 "paths": { 452 - "/r/{id}": { 452 + "/go/{id}": { 453 453 "get": { 454 454 "summary": "Redirect to Link URL", 455 455 "description": "Redirects to the URL associated with the given link ID. This is the public shortlink endpoint.", ··· 1489 1489 "/link/{id}": { 1490 1490 "get": { 1491 1491 "summary": "Redirect to a Link (Deprecated)", 1492 - "description": "DEPRECATED: Use GET /r/{id} instead. Redirects to the URL associated with the given ID.", 1492 + "description": "DEPRECATED: Use GET /go/{id} instead. Redirects to the URL associated with the given ID.", 1493 1493 "deprecated": true, 1494 1494 "tags": ["Deprecated"], 1495 1495 "parameters": [
+3 -3
internal/handler/api_v1_redirect.go
··· 8 8 "strings" 9 9 ) 10 10 11 - // APIv1RedirectHandler handles GET /r/{id} - the public shortlink redirect. 11 + // APIv1RedirectHandler handles GET /go/{id} - the public shortlink redirect. 12 12 // This redirects users to the actual URL associated with a link ID. 13 13 // If a valid click signature is provided via the sig query parameter, 14 14 // the click count is incremented asynchronously. ··· 21 21 22 22 ctx := r.Context() 23 23 24 - // Parse ID from path: /r/{id} 24 + // Parse ID from path: /go/{id} 25 25 path := r.URL.Path 26 - idStr := strings.TrimPrefix(path, "/r/") 26 + idStr := strings.TrimPrefix(path, "/go/") 27 27 28 28 // Check if we got a valid ID string 29 29 if idStr == "" || idStr == path {
+16 -16
internal/handler/api_v1_redirect_test.go
··· 53 53 }{ 54 54 { 55 55 name: "valid ID redirects with 302", 56 - path: "/r/123", 56 + path: "/go/123", 57 57 linkURL: "https://example.com/article", 58 58 expectedStatus: http.StatusFound, 59 59 expectedLocation: "https://example.com/article", 60 60 }, 61 61 { 62 62 name: "valid ID with http scheme redirects", 63 - path: "/r/456", 63 + path: "/go/456", 64 64 linkURL: "http://example.com/page", 65 65 expectedStatus: http.StatusFound, 66 66 expectedLocation: "http://example.com/page", 67 67 }, 68 68 { 69 69 name: "invalid ID returns 400", 70 - path: "/r/abc", 70 + path: "/go/abc", 71 71 expectedStatus: http.StatusBadRequest, 72 72 checkBody: func(t *testing.T, body string) { 73 73 if body != "Invalid ID\n" { ··· 77 77 }, 78 78 { 79 79 name: "empty ID returns 400", 80 - path: "/r/", 80 + path: "/go/", 81 81 expectedStatus: http.StatusBadRequest, 82 82 checkBody: func(t *testing.T, body string) { 83 83 if body != "Invalid ID\n" { ··· 87 87 }, 88 88 { 89 89 name: "negative ID returns 400", 90 - path: "/r/-5", 90 + path: "/go/-5", 91 91 expectedStatus: http.StatusBadRequest, 92 92 checkBody: func(t *testing.T, body string) { 93 93 if body != "Invalid ID\n" { ··· 97 97 }, 98 98 { 99 99 name: "non-existent link returns 404", 100 - path: "/r/999", 100 + path: "/go/999", 101 101 linkURLErr: errors.New("link not found"), 102 102 expectedStatus: http.StatusNotFound, 103 103 }, 104 104 { 105 105 name: "store returns not found error returns 404", 106 - path: "/r/888", 106 + path: "/go/888", 107 107 linkURLFn: func(id int) (string, error) { 108 108 return "", errors.New("record not found") 109 109 }, ··· 111 111 }, 112 112 { 113 113 name: "javascript scheme is blocked", 114 - path: "/r/123", 114 + path: "/go/123", 115 115 linkURL: "javascript:alert(1)", 116 116 expectedStatus: http.StatusBadRequest, 117 117 checkBody: func(t *testing.T, body string) { ··· 122 122 }, 123 123 { 124 124 name: "data scheme is blocked", 125 - path: "/r/123", 125 + path: "/go/123", 126 126 linkURL: "data:text/html,<script>alert(1)</script>", 127 127 expectedStatus: http.StatusBadRequest, 128 128 checkBody: func(t *testing.T, body string) { ··· 133 133 }, 134 134 { 135 135 name: "file scheme is blocked", 136 - path: "/r/123", 136 + path: "/go/123", 137 137 linkURL: "file:///etc/passwd", 138 138 expectedStatus: http.StatusBadRequest, 139 139 checkBody: func(t *testing.T, body string) { ··· 191 191 }{ 192 192 { 193 193 name: "valid signature increments clicks", 194 - path: "/r/123", 194 + path: "/go/123", 195 195 clickSigningKey: "testsecret", 196 196 linkURL: "https://example.com", 197 197 expectIncrement: true, ··· 199 199 }, 200 200 { 201 201 name: "invalid signature does not increment", 202 - path: "/r/123", 202 + path: "/go/123", 203 203 sigQueryParam: "invalidsig", 204 204 clickSigningKey: "testsecret", 205 205 linkURL: "https://example.com", ··· 208 208 }, 209 209 { 210 210 name: "missing signature does not increment", 211 - path: "/r/123", 211 + path: "/go/123", 212 212 sigQueryParam: "", 213 213 clickSigningKey: "testsecret", 214 214 linkURL: "https://example.com", ··· 217 217 }, 218 218 { 219 219 name: "no signing key configured does not increment", 220 - path: "/r/123", 220 + path: "/go/123", 221 221 clickSigningKey: "", 222 222 linkURL: "https://example.com", 223 223 expectIncrement: false, ··· 283 283 284 284 for _, method := range methods { 285 285 t.Run(method, func(t *testing.T) { 286 - req := httptest.NewRequest(method, "/r/123", nil) 286 + req := httptest.NewRequest(method, "/go/123", nil) 287 287 w := httptest.NewRecorder() 288 288 289 289 handler.APIv1RedirectHandler(w, req) ··· 304 304 Config: &config.Config{}, 305 305 } 306 306 307 - req := httptest.NewRequest(http.MethodHead, "/r/123", nil) 307 + req := httptest.NewRequest(http.MethodHead, "/go/123", nil) 308 308 w := httptest.NewRecorder() 309 309 310 310 handler.APIv1RedirectHandler(w, req)