Use permanent redirects for trailing slash normalization
Merged
RI
20h ago+121+121+012345678901234567890123456789-2-2-0123456789
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()inapps/shared/trailingSlashRedirect.tsand return 301 whenASSETS.fetchresponds with 307 that only adds a trailing slash (query string preserved viaredirectToPath). - Skip static files (last path segment contains
.) and/api/*. - This keeps existing
_redirectsbehavior for slashless sources such as/self-hosted-capgo(still 302 to docs). - Set
html_handling: "force-trailing-slash"inapps/web/wrangler.jsoncto align with Astro. - Tests in
apps/web/test/trailing-slash-redirect.test.js. - Also fix a truncated
\ud83d...escape ingithub-releases.jsonthat 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.


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.