Use permanent redirects for trailing slash normalization

Merged
RI
20h ago
+121-2

Problem

Slashless marketing URLs on capgo.app returned HTTP 307 to the trailing-slash form, which can split SEO signals from explicit 301 redirects elsewhere (for example /capawesome/).

Root cause

The web app uses Cloudflare Workers static assets (wrangler.jsonc assets binding). Default html_handling (auto-trailing-slash) issues 307 when a path maps to a folder index.html (Astro trailingSlash: 'always'). The worker runs first (run_worker_first: true) and forwards slashless page URLs to ASSETS.fetch, so clients see that temporary redirect.

Fix

  • Add permanentTrailingSlashLocation() in apps/shared/trailingSlashRedirect.ts and return 301 when ASSETS.fetch responds with 307 that only adds a trailing slash (query string preserved via redirectToPath).
  • Skip static files (last path segment contains .) and /api/*.
  • This keeps existing _redirects behavior for slashless sources such as /self-hosted-capgo (still 302 to docs).
  • Set html_handling: "force-trailing-slash" in apps/web/wrangler.jsonc to align with Astro.
  • Tests in apps/web/test/trailing-slash-redirect.test.js.
  • Also fix a truncated \ud83d... escape in github-releases.json that broke Vite JSON parsing in CI.

Verified status codes

Before (production, 2026-10-18):

curl -sI https://capgo.app/pricing
HTTP/2 307
location: /pricing/

curl -sI https://capgo.app/capawesome/
HTTP/2 301
location: https://capgo.app/alternatives/capawesome/

After (local worker tests):

bun test apps/web/test/trailing-slash-redirect.test.js
# ASSETS 307 /pricing/ -> worker 301 Location: https://capgo.app/pricing/
# /self-hosted-capgo stays 302 to /docs/plugins/updater/self-hosted/getting-started/

Preview deploy should show 301 for slashless page URLs such as /pricing and /fr/pricing.

Open in Web Open in Cursor 

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Summary by CodeRabbit

  • Bug Fixes
    • Page URLs without a trailing slash now redirect permanently to the slash-ending version, preserving query strings and fragments. Root paths, API routes, and static asset paths are excluded.
    • Existing redirects that do not meet these conditions, including temporary redirects, remain unchanged.