url-canonicalization-redirects

star 2

URL 정규화·중복 제거·리다이렉트 규칙을 프레임워크/인프라 레이어별로 통일하는 가이드. http→https, www/non-www, 트레일링 슬래시, 대소문자, 쿼리 파라미터, 트래킹 파라미터, 디폴트 파일명까지 7대 정규화 축을 일관되게 처리한다. Next.js / Vite SPA / Vercel / Netlify / Cloudflare Pages / Nginx / Apache 패턴을 다룬다. <example>사용자: "www → non-www + http → https + 트레일링 슬래시 제거를 Next.js에서 한 번에 처리하려면?"</example> <example>사용자: "utm_source, fbclid 같은 트래킹 파라미터가 sitemap.xml과 다른 URL로 색인되고 있는데 어떻게 정리하나?"</example> <example>사용자: "Vite SPA 정적 빌드인데 호스팅이 Netlify야. 슬래시 통일 어떻게 해?"</example>

puk0806 By puk0806 schedule Updated 6/3/2026

name: url-canonicalization-redirects description: > URL 정규화·중복 제거·리다이렉트 규칙을 프레임워크/인프라 레이어별로 통일하는 가이드. http→https, www/non-www, 트레일링 슬래시, 대소문자, 쿼리 파라미터, 트래킹 파라미터, 디폴트 파일명까지 7대 정규화 축을 일관되게 처리한다. Next.js / Vite SPA / Vercel / Netlify / Cloudflare Pages / Nginx / Apache 패턴을 다룬다. 사용자: "www → non-www + http → https + 트레일링 슬래시 제거를 Next.js에서 한 번에 처리하려면?" 사용자: "utm_source, fbclid 같은 트래킹 파라미터가 sitemap.xml과 다른 URL로 색인되고 있는데 어떻게 정리하나?" 사용자: "Vite SPA 정적 빌드인데 호스팅이 Netlify야. 슬래시 통일 어떻게 해?"

URL 정규화·리다이렉트 가이드

소스:

검증일: 2026-06-02 검증 대상 버전: Next.js 16.x, Astro 5.x, Vercel/Netlify/Cloudflare Pages 2026-06 시점 문서


0. TL;DR — 결정 트리

결정 권장
정규화 레이어 CDN/Edge > Origin(Nginx) > 앱(Next.js middleware) > 클라이언트(금지) — 사용자에게 빨리 도착할수록 좋다
영구 이전 신호 301 (GET 위주) / 308 (POST 등 메서드 보존 필요)
임시 이전 신호 302 / 307 (메서드 보존 필요 시)
정규화 신호 우선순위 redirect (강) > rel=canonical (강·hint) > sitemap (약) — Google 공식
redirect chain 1 hop이 이상, 최대 5 hops 미만 (Google John Mueller)
트래킹 파라미터(utm/fbclid) 사용자에게 redirect 하지 말 것. canonical로 파라미터 없는 URL을 명시
트레일링 슬래시 슬래시 유/무 어느 쪽이든 무방. 일관성이 본질. 다른 형태는 301로 통일

1. URL 정규화 7대 축

같은 콘텐츠가 여러 URL로 도달 가능하면 SEO 신호가 분산되고 색인 예산이 낭비된다. 다음 7개 축을 모두 한 방향으로 통일해야 한다.

두 형태 결정 원칙
1. 프로토콜 http:// vs https:// 무조건 https (Google이 자동 선호)
2. 호스트 www.example.com vs example.com 어느 쪽이든 무방, 일관성
3. 트레일링 슬래시 /about vs /about/ 어느 쪽이든 무방, 일관성
4. 대소문자 /About vs /about 소문자 권장 (대부분 OS는 case-sensitive)
5. 파라미터 순서 ?a=1&b=2 vs ?b=2&a=1 알파벳순 정렬 또는 무시
6. 트래킹 파라미터 ?utm_source=x canonical에서 제거, 사용자 URL은 유지
7. 디폴트 파일명 /index.html vs / /index.html/ 로 301

출처: Google Search Central — Consolidate duplicate URLs

자가 진단 체크리스트

다음 8개 URL이 모두 같은 최종 URL에 도달하는지 curl로 확인하라:

# 모두 https://example.com/about 로 단일 301 도달해야 정상
curl -I http://example.com/about
curl -I http://www.example.com/about
curl -I https://www.example.com/about
curl -I https://example.com/about/
curl -I https://example.com/About
curl -I https://example.com/about?utm_source=twitter
curl -I https://example.com/about/index.html
curl -I https://example.com//about    # 중복 슬래시

각각이 Location: 헤더로 직접 최종 URL을 가리켜야 한다 (중간 hop 없이).


2. HTTP 상태 코드 — 301 vs 302 vs 307 vs 308

코드 이름 영구/임시 메서드 보존 사용 케이스
301 Moved Permanently 영구 ❌ (POST→GET 가능) 사이트 재구성, 도메인 이전, www/슬래시 정규화 (GET)
302 Found 임시 ❌ (POST→GET 가능) A/B 테스트, 점검 페이지
303 See Other 임시 ❌ (모두 GET 변환) POST 처리 후 결과 페이지 표시 (Post/Redirect/Get 패턴)
307 Temporary Redirect 임시 API 임시 이전, 메서드 보존 필요 시
308 Permanent Redirect 영구 API 영구 이전, POST/PUT 영구 이전

출처: MDN — HTTP Redirections (https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections)

결정 트리

이 redirect는 영구적인가?
├─ 예 (영구)
│   └─ POST/PUT/DELETE 등 비-GET 메서드도 처리해야 하나?
│       ├─ 예 → 308
│       └─ 아니오 (GET만) → 301
└─ 아니오 (임시)
    └─ POST/PUT/DELETE 등 비-GET 메서드도 처리해야 하나?
        ├─ 예 → 307
        └─ 아니오 (GET만) → 302

주의: 일반 웹페이지(HTML) 정규화는 GET 위주이므로 301이면 충분하다. API 엔드포인트 이전이라면 308을 고려한다.


3. canonical 태그 vs redirect — 신호 강도

Google이 인식하는 정규화 신호 강도 순서 (공식):

1. redirect (강한 신호)         ← 가능한 한 이것을 우선
2. rel="canonical" (강한 신호, hint)
3. sitemap.xml 포함 (약한 신호)

출처: Google Search Central — "redirects (strong), rel=canonical (strong), sitemap (weak)"

충돌 시 동작

신호들이 일치하지 않으면 Google이 자기 판단으로 canonical을 선택한다.

나쁜 예 (충돌):
/old → 301 → /new
/new 의 canonical = /old   ← 모순. 신호 약화

좋은 예:
/old → 301 → /new
/new 의 canonical = /new   ← 자기참조. 일관성

canonical 운영 규칙

  • 모든 페이지는 자기참조 canonical을 기본으로 한다 (페이지네이션 포함)
  • 절대 URL 사용 (https://example.com/...)
  • HTTPS, 선호 호스트, 트레일링 슬래시 정책에 일치해야 함
  • JavaScript로 canonical 변경 금지 — HTML 소스에 박는다
  • robots.txt로 정규화 시도 금지 (Google이 비권장 명시)

페이지네이션

<!-- /blog?page=2 -->
<link rel="canonical" href="https://example.com/blog?page=2" />
<!-- rel=prev/next는 사용하지 않는다 (Google이 2019년 deprecated 공식 발표) -->

Google이 2019년 3월 rel=prev/next를 인덱싱 신호로 사용 중단 발표. 페이지네이션 페이지는 자기참조 canonical을 사용한다.


4. 트레일링 슬래시 — 슬래시 유 vs 무

SEO에는 어느 쪽도 무방하다. 일관성이 본질이고, 반대편 형태는 301로 정규화해야 한다.

프레임워크 기본 정책 변경 방법
Next.js 슬래시 (/about//about) next.config.js 에서 trailingSlash: true
Astro 슬래시 (build.format: 'directory' 기본) trailingSlash: 'never' + build.format: 'file'
Vite SPA 호스팅 레이어에 위임 Vercel/Netlify/Nginx 설정
Nuxt 슬래시 routeRules로 조정

Next.js trailingSlash 동작 상세

// next.config.js — 기본 (슬래시 없음)
module.exports = {
  // trailingSlash: false  // 기본
};
// /about/ → 308 → /about

// next.config.js — 슬래시 있음
module.exports = {
  trailingSlash: true,
};
// /about → 308 → /about/

Next.js는 내부 정규화 redirect로 308을 사용한다 (메서드 보존). 예외: 확장자 있는 정적 파일(/file.txt), .well-known/ 경로는 슬래시가 추가되지 않는다.

미들웨어에서 커스텀 슬래시 처리를 하려면 skipTrailingSlashRedirect로 Next.js의 자동 정규화를 끄고 직접 처리한다.


5. Next.js 정규화 패턴

5-1. 정적 redirect (next.config.js)

가장 단순한 케이스. 빌드 타임에 결정되는 redirect.

// next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  trailingSlash: false,

  async redirects() {
    return [
      // 도메인 내 path 정규화
      {
        source: '/old-blog/:slug',
        destination: '/news/:slug',
        permanent: true,           // → 308
      },
      {
        source: '/About',
        destination: '/about',
        permanent: true,
      },
    ];
  },
};

permanent: true → 308, permanent: false → 307. 일반 SEO 정규화는 permanent: true 사용.

5-2. 동적 호스트 정규화 (middleware)

next.config.jsredirects()path 기반이라 호스트 정규화(www/non-www, http/https)에는 적합하지 않다. middleware에서 처리한다.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

const CANONICAL_HOST = 'example.com';

export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone();
  const host = request.headers.get('host') ?? '';

  // 1) www → non-www
  if (host.startsWith('www.')) {
    url.host = host.replace(/^www\./, '');
    return NextResponse.redirect(url, 308);
  }

  // 2) 대문자 path → 소문자
  if (url.pathname !== url.pathname.toLowerCase()) {
    url.pathname = url.pathname.toLowerCase();
    return NextResponse.redirect(url, 308);
  }

  // 3) 트래킹 파라미터는 redirect하지 않고 그대로 둔다
  //    canonical 메타에서 제거하는 방식 (섹션 8 참조)

  return NextResponse.next();
}

export const config = {
  matcher: [
    // 정적 파일과 _next 내부 경로 제외
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

주의 1: Vercel 등 CDN 앞단에서 https 종단이 이미 처리되므로 middleware에서 http→https는 보통 처리하지 않는다. Origin 서버를 직접 노출하는 경우에만 필요하다.

주의 2: middleware는 모든 요청에 실행되어 비용이 든다. CDN 레이어에서 처리 가능하면 그쪽이 우선이다.

5-3. canonical 메타 (App Router)

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
  const { slug } = await params;
  return {
    alternates: {
      canonical: `https://example.com/blog/${slug}`,
    },
  };
}

주의 (DISPUTED): Next.js 14/15에서 metadata.alternates.canonical문자열로 지정하면 trailing slash 정책이 사용자 의도와 다르게 normalize되는 이슈 보고가 있다 (next.js Discussion #65323). 절대 URL을 직접 박고, trailingSlash 옵션과 일치하는 형태로 작성하면 안전하다.


6. Vite SPA + 호스팅 레이어

Vite 자체에는 서버 정규화가 없다. 정적 빌드 결과를 어디에 올리느냐에 따라 정규화 위치가 달라진다.

6-1. Vercel (vercel.json)

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "redirects": [
    {
      "source": "/old-page",
      "destination": "/new-page",
      "permanent": true
    },
    {
      "source": "/old-blog/:slug",
      "destination": "/news/:slug",
      "permanent": true
    }
  ]
}

Vercel은 permanent: true308, permanent: false307 (Vercel 공식). www/non-www 정규화는 Vercel 대시보드 Domains 설정에서 한쪽을 primary로 지정 → 자동 308. /.well-known 경로는 정규화 대상에서 제외된다 (Vercel 문서 명시). redirects 배열 한계: 2,048개 / source·destination 문자열 4,096자.

6-2. Netlify (_redirects)

public/_redirects (Vite 기준) 또는 빌드 출력 루트에 파일을 둔다.

# _redirects
# 형식: <from>  <to>  [status_code]  [options]

# 도메인 정규화 (강제 — ! 접미사로 매칭 파일보다 우선)
http://example.com/*    https://example.com/:splat   301!
http://www.example.com/* https://example.com/:splat  301!
https://www.example.com/* https://example.com/:splat 301!

# path 정규화
/old-blog/*    /news/:splat   301
/About         /about         301

주의 (Netlify 한정): _redirects 만으로 트레일링 슬래시 추가/제거 는 불가능하다 (Netlify 공식 명시). Netlify가 내부적으로 "Pretty URLs"로 자동 정규화하므로 슬래시 유무는 Netlify 동작에 맡기는 방식이 안전하다.

6-3. Cloudflare Pages (_redirects)

# _redirects
# 지원 status: 301, 302, 303, 307, 308 (기본 302)

/old-blog/*   /news/:splat   301
/about/       /about         301

주의 (Cloudflare Pages 한정): 도메인 레벨 정규화(www, http→https), 쿼리 파라미터 기반 정규화, 국가/언어 조건은 _redirects 에서 지원되지 않는다. 이 경우 Cloudflare Bulk Redirects 또는 Single Redirects Rules를 사용해야 한다. 한도: static 2,000개 + dynamic 100개 = 총 2,100개. 줄당 1,000자.

6-4. 호스팅 비교표

호스팅 슬래시 자동 정규화 도메인 정규화 한도
Vercel 프로젝트 도메인 설정 대시보드 primary 2,048 redirects
Netlify 자동 (Pretty URLs) _redirects 또는 사이트 설정 무제한 (실용상)
Cloudflare Pages 수동 Pages 외 Rules 필요 2,100 합계


상세 레퍼런스 (예제·고급 패턴·흔한 실수) → references/REFERENCE.md

Install via CLI
npx skills add https://github.com/puk0806/gugbab-claude --skill url-canonicalization-redirects
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator