Astro URL 뒤 슬래시 정리하는 법
Astro URL 뒤 슬래시 정리하는 법을 기록한다. /blog/글과 /blog/글/이 둘 다 열리면 사람 눈엔 같은 페이지지만 검색엔진 눈엔 다른 주소 두 개다. 영어로 대이주한 글 이후, 사이트 주소를 canonical·hreflang·사이트맵과 똑같은 뒤 슬래시 형태 하나로 통일한 과정이다.
왜 URL 뒤 슬래시를 통일해야 하나
이 사이트는 사이트맵·canonical·hreflang이 전부 /blog/글/처럼 슬래시 붙은 형태로 나간다. 그런데 슬래시 없는 /blog/글도 그냥 열린다면, 같은 내용이 주소 두 개로 존재하는 셈이다. 검색엔진은 이걸 중복 콘텐츠로 보고 순위를 나눠 가진다.
Cloudflare가 알아서 슬래시를 붙여주긴 한다. 문제는 그게 307, 임시 이동이라는 것. 임시라서 검색엔진이 “이 주소가 진짜다”라고 통합해주지 않는다. 영구 이전은 301, 영구 이동이어야 한다. 그래서 붙여주는 주체를 정적 자산에서 워커로 바꿔서, 코드가 직접 301을 내주게 했다.
없는 페이지까지 넘기면 “301 후 404”가 된다
처음엔 “슬래시 없으면 무조건 붙여서 넘겨”로 짰다. 그랬더니 오타 주소도 슬래시를 붙여 넘긴 뒤 404가 떴다. 404로 끝나는 리다이렉트는 검색엔진이 제일 싫어하는 패턴이다.
그래서 실제로 있는 페이지일 때만 넘기게 고쳤다. 워커가 슬래시 붙은 주소로 HEAD 요청을 한 번 던져보고, 응답이 정상일 때만 301을 낸다. 파일 확장자가 붙은 주소(robots.txt 등)는 애초에 건드리지 않는다. 쿼리스트링(?cat=site)은 그대로 달고 넘긴다.
if (!url.pathname.endsWith('/') && !/\.[^/]+$/.test(url.pathname)) {
const slashed = url.origin + url.pathname + '/';
const probe = await env.ASSETS.fetch(new Request(slashed, { method: 'HEAD' }));
if (probe.ok) return Response.redirect(slashed + url.search, 301);
}
구 주소(/blog/002-... → /kr/blog/002-.../)도 같은 원칙으로, 슬래시까지 붙여 한 번에 목적지에 도착시킨다. 리다이렉트가 두 번 튀는 체인은 만들지 않는다.
run_worker_first 함정 — 라이브에서만 조용히 안 먹던 몬스터
코드를 다 짜고 배포했는데, 로컬에선 되고 라이브에선 안 됐다. 범인은 wrangler.jsonc의 딱 한 줄이었다.
Cloudflare는 기본적으로 정적 자산을 워커보다 먼저 응답시킨다. 그러면 내 301 코드가 실행될 기회조차 없이 자산이 307로 먼저 답해버린다. 코드는 멀쩡한데 라이브에서만 조용히 안 먹는, 제일 잡기 싫은 종류의 버그다.
"assets": {
"directory": "./dist",
"binding": "ASSETS",
"run_worker_first": true // 워커가 먼저 받게 — 이 한 줄이 없으면 리다이렉트가 안 산다
}
run_worker_first: true를 켜면 워커가 먼저 요청을 받고, 리다이렉트 대상이 아닌 것만 env.ASSETS.fetch로 넘긴다.
canonical·hreflang도 같은 슬래시로
주소만 슬래시로 통일하고 <head>의 canonical이 슬래시 없이 나가면 서로 어긋난다. 그래서 Base.astro에 작은 헬퍼 하나를 뒀다 — 경로를 받으면 끝에 슬래시가 없을 때만 붙여준다. canonical과 hreflang 모두 이 헬퍼를 통과하니, 컴포넌트에서 슬래시 유무를 신경 쓸 필요가 없다.
const abs = (p) => {
const u = p.startsWith('http') ? p : site + p;
return u.endsWith('/') ? u : u + '/';
};
스모크 테스트로 재발 방지
이런 규칙은 눈으로 안 보여서, 나중에 무심코 설정 한 줄 지우면 조용히 깨진다. 그래서 curl로 상태코드를 확인하는 작은 스모크 테스트를 붙였다. 확인하는 것:
- 슬래시 없는 주소 → 307이 아니라 301로 슬래시 붙은 주소
- 쿼리스트링(
?cat=site)이 리다이렉트 후에도 유지되는지 - 구 주소 → 슬래시까지 붙여 한 번에 도착 (체인 금지)
- 정상 페이지·
robots.txt·사이트맵은 200 그대로 - 없는 주소는 리다이렉트 없이 바로 404
배포 후 이 스크립트에 라이브 주소를 물려 한 번 돌리면, run_worker_first 같은 함정이 다시 살아났는지 즉시 잡힌다.
결과
- ✅ 모든 주소가 canonical·hreflang·사이트맵과 같은 뒤 슬래시 형태로 301 통일
- ✅ 오타 주소는 “301 후 404” 없이 바로 404
- ✅
run_worker_first한 줄 + 스모크 테스트로 재발 방지 - ⌨️ 내가 친 코드: 이번에도 0줄
주소에 슬래시 하나 붙이고 말고가 뭐 대수냐 싶지만, 검색엔진에겐 이게 “같은 집 주소 하나”냐 “헷갈리는 주소 두 개”냐의 차이다. 그리고 이런 규칙일수록, 라이브에서 직접 curl을 때려봐야 몬스터가 튀어나온다.