birchholt 직접 해보고 남기는 기록

커스텀 도메인을 붙여도 pages.dev 주소는 계속 열려 있습니다

이 사이트는 Cloudflare Pages에 올라가 있고 birchholt.com이라는 커스텀 도메인(내가 산 주소를 호스팅에 연결한 것)을 붙여 씁니다. 도메인을 붙이면 처음에 받은 birchholt.pages.dev 주소는 뒤로 물러날 줄 알았는데 그 주소는 지금도 똑같이 열립니다.

새 간판을 단 가게 옆으로 예전 쪽문이 여전히 열려 있고 같은 진열대가 들여다보이는 골목

같은 글이 주소 두 개로 열리면 검색엔진 입장에서는 중복 페이지입니다. 막아둔 게 있는지 확인해보니 아무것도 없었습니다. 그래서 _headers 파일로 막는 방법을 찾고 운영 사이트에 넣기 전에 미리보기 배포에 올려서 규칙이 어느 주소에 붙는지 직접 찍어봤습니다. 그다음 production에 넣고 두 주소를 다시 찍었습니다.

같은 페이지가 두 주소에서 나오고 있었습니다

쌍둥이처럼 똑같은 편지 두 통이 서로 다른 우편함에 꽂혀 있는 시골 길가

먼저 응답 헤더(서버가 본문과 함께 보내는 부가 정보)를 봤습니다. 색인을 막는 헤더는 X-Robots-Tag입니다.

$ curl -sI https://birchholt.com/ | grep -iE '^(HTTP|x-robots)'
HTTP/2 200

$ curl -sI https://birchholt.pages.dev/ | grep -iE '^(HTTP|x-robots)'
HTTP/2 200

둘 다 X-Robots-Tag가 없습니다. 내용이 정말 같은지도 확인했습니다.

$ curl -s https://birchholt.com/ | md5
8d1b8e4ddf12070863f2ab8e397db6c2
$ curl -s https://birchholt.pages.dev/ | md5
8d1b8e4ddf12070863f2ab8e397db6c2

바이트 하나 다르지 않습니다. 배포 하나에 도메인 두 개가 연결돼 있으니까요.

그나마 다행인 건 canonical(이 페이지의 원본 주소가 어디인지 검색엔진에 알려주는 표시)입니다. Hugo 설정의 baseURL이 https://birchholt.com/이라서 pages.dev로 열어도 원본은 .com을 가리킵니다.

$ curl -s https://birchholt.pages.dev/posts/soft-404-on-pages/ | grep -oE '<link rel=canonical[^>]*>'
<link rel=canonical href=https://birchholt.com/posts/soft-404-on-pages/>

다만 canonical은 검색엔진에 건네는 신호이지 명령이 아닙니다. 어느 주소를 원본으로 고를지는 결국 Google이 정합니다.

Google · Consolidate duplicate URLs — 「rel="canonical" link annotations: A strong signal that the specified URL should become canonical.」 이 문서는 영향력이 큰 순서로 방법을 나열하는데, 리다이렉트가 canonical보다 앞에 있습니다. 2026-10-04에 확인했습니다.

미리보기 주소에는 이미 noindex가 붙어 있었습니다

마당 뒤편 작은 창고들 문마다 같은 모양의 빈 나무 패가 걸려 있고 본채 대문 고리만 비어 있는 풍경

Pages는 배포할 때마다 해시.birchholt.pages.dev 같은 고유 주소를 하나씩 만듭니다. 여기도 같겠거니 하고 찍었는데 결과가 달랐습니다.

$ curl -sI https://fbf98b55.birchholt.pages.dev/ | grep -iE '^(HTTP|x-robots)'
HTTP/2 200
x-robots-tag: noindex

fbf98b55는 그때 birchholt.com에 나가고 있던 production 배포입니다. 그 직전 production 배포의 해시 주소도 같은 결과였습니다. 저는 _headers 파일을 둔 적이 없으니 Cloudflare가 기본으로 붙이는 겁니다.

문서에는 미리보기 배포에 붙는다고만 적혀 있습니다.

Cloudflare Pages · Preview deployments — 「By default, every preview deployment generated by Cloudflare Pages includes the X-Robots-Tag: noindex HTTP response header.」 2026-10-04에 확인했습니다. production 배포의 해시 주소에도 붙는다는 건 문서에 없고, 위 curl로 직접 본 것입니다.

정리하면 빈틈은 맨 birchholt.pages.dev 하나입니다. 해시가 붙은 주소는 이미 막혀 있고 아무것도 안 붙은 프로젝트 주소만 열려 있습니다.

_headers 한 블록이면 된다고 문서에 나와 있습니다

문틀 위쪽에 작은 쪽지를 끼워 넣어 그 문으로만 들어오는 손님에게 안내가 가도록 하는 손

Cloudflare Pages는 빌드 결과물 맨 위에 _headers라는 파일(확장자 없음)이 있으면 그 안의 규칙대로 응답 헤더를 붙입니다. 규칙의 주소 자리에 도메인까지 들어간 절대 주소를 쓸 수 있고 문서 예시가 바로 이 경우입니다.

https://:project.pages.dev/*
  X-Robots-Tag: noindex

:project는 자리표시자(아무 값이나 받아주는 빈칸)입니다. 문서의 예시 표를 보면 https://custom.domain/... 요청에는 이 헤더가 안 붙고 https://myproject.pages.dev/... 요청에만 붙습니다. 같은 배포라도 어느 도메인으로 들어왔느냐에 따라 헤더가 달라지는 겁니다.

Cloudflare Pages · Headers, 2026-10-04에 확인했습니다. 같은 문서의 「Prevent your workers.dev URLs showing in search results」 항목은 제목이 workers.dev인데, 본문은 *.pages.dev와 *.*.pages.dev를 막는 예시로 위 블록에 https://:version.:project.pages.dev/* 블록 하나를 더 붙여 둡니다.

Hugo에서는 static/_headers에 두면 됩니다. static/ 안의 파일은 빌드할 때 결과물 폴더에 그대로 복사됩니다.

$ hugo --gc --minify
...
$ cat public/_headers
https://:project.pages.dev/*
  X-Robots-Tag: noindex

hugo v0.165.0 extended, 2026-10-04. 운영 저장소는 그대로 두고 복사본에서 빌드했습니다.

운영에 넣기 전에 미리보기 배포로 올려봤습니다

본채 정원에 심기 전 뒤뜰 작은 화분에 먼저 꽂아 둔 묘목 두 그루와 색이 다른 끈

이 규칙은 production에 올려야 birchholt.pages.dev에 붙습니다. 하지만 확인도 안 한 헤더를 운영 사이트에 바로 넣고 싶지는 않았습니다. birchholt.com에 noindex가 잘못 붙으면 사이트 전체가 검색에서 빠질 수 있기 때문입니다.

그래서 production이 아닌 브랜치로 미리보기 배포를 올렸습니다. 여기서 하나 걸렸습니다. 미리보기는 원래 noindex라서, 규칙이 붙었는지 안 붙었는지 헤더만 봐서는 구별이 안 됩니다.

그래서 규칙마다 표식 헤더를 하나씩 달았습니다.

https://:project.pages.dev/*
  X-Robots-Tag: noindex
  X-Lab-Match: project

https://:version.:project.pages.dev/*
  X-Lab-Match: version
$ npx --yes wrangler@latest pages deploy public --project-name birchholt --branch noindex-test --commit-dirty=true
...
✨ Uploading _headers
✨ Deployment complete! Take a peek over at https://c07c060f.birchholt.pages.dev
✨ Deployment alias URL: https://noindex-test.birchholt.pages.dev

--branch에 production 브랜치(main)가 아닌 이름을 주면 미리보기로 올라갑니다. 결과는 이랬습니다.

$ curl -sI https://c07c060f.birchholt.pages.dev/ | grep -iE '^(x-robots|x-lab)'
x-lab-match: version
x-robots-tag: noindex

$ curl -sI https://noindex-test.birchholt.pages.dev/ | grep -iE '^(x-robots|x-lab)'
x-lab-match: version
x-robots-tag: noindex

project 표식이 어디에도 없습니다. https://:project.pages.dev/* 규칙은 해시 주소에도, 브랜치 이름 주소에도 붙지 않았습니다. 붙은 건 :version.:project 규칙뿐이고 그 규칙에는 X-Robots-Tag를 넣지 않았으니 보이는 noindex는 Cloudflare가 기본으로 붙인 것입니다.

문서의 자리표시자 설명과 맞습니다. 호스트 부분의 자리표시자는 점(.)을 넘지 못하니 :project가 c07c060f.birchholt처럼 점이 낀 값을 받을 수 없습니다.

같은 Headers 문서 — 「Placeholders match all characters apart from the delimiter, which when part of the host, is a period (.) or a forward-slash (/)」. 그리고 _headers 파일 자체는 주소로 열리지 않습니다. https://c07c060f.birchholt.pages.dev/_headers는 404였습니다(2026-10-04).

production에 넣은 뒤에 이렇게 확인합니다

새로 단 문패를 확인하러 앞문과 쪽문을 차례로 돌아보며 손전등을 비추는 사람

미리보기로 알게 된 건 이 규칙이 해시 주소에는 안 붙는다는 것까지였습니다. 정작 원하는 두 가지, birchholt.pages.dev에는 붙고 birchholt.com에는 안 붙는다는 건 production에 올려야 볼 수 있습니다. 미리보기 배포는 그 두 주소로 나가지 않기 때문입니다.

그래서 production에 넣을 파일은 문서 첫 블록 하나만으로 정했습니다.

https://:project.pages.dev/*
  X-Robots-Tag: noindex

해시 주소는 이미 기본값으로 막혀 있으니 :version 블록은 넣지 않았습니다. 이 두 줄을 static/_headers로 넣어 2026-10-04에 production으로 배포했습니다.

배포한 뒤 같은 경로를 두 도메인에 똑같이 찍었습니다. 홈과 글 페이지만이 아니라 이미지, RSS(index.xml), 없는 주소까지 넣었습니다.

$ for h in birchholt.pages.dev birchholt.com; do
    for p in / /posts/soft-404-on-pages/ /images/posts/soft-404-on-pages/00.webp /index.xml /no-such-page/; do
      echo "$h$p  $(curl -sI https://$h$p | grep -iE '^(HTTP|x-robots-tag)' | tr -d '\r' | tr '\n' ' ')"
    done
  done
birchholt.pages.dev/  HTTP/2 200  x-robots-tag: noindex
birchholt.pages.dev/posts/soft-404-on-pages/  HTTP/2 200  x-robots-tag: noindex
birchholt.pages.dev/images/posts/soft-404-on-pages/00.webp  HTTP/2 200  x-robots-tag: noindex
birchholt.pages.dev/index.xml  HTTP/2 200  x-robots-tag: noindex
birchholt.pages.dev/no-such-page/  HTTP/2 404  x-robots-tag: noindex
birchholt.com/  HTTP/2 200
birchholt.com/posts/soft-404-on-pages/  HTTP/2 200
birchholt.com/images/posts/soft-404-on-pages/00.webp  HTTP/2 200
birchholt.com/index.xml  HTTP/2 200
birchholt.com/no-such-page/  HTTP/2 404

pages.dev 쪽은 다섯 줄 모두 noindex이고 birchholt.com 쪽은 한 줄도 없습니다. 문서 예시 표대로 나왔습니다. 없는 주소의 404 응답에도 헤더가 붙는 건 이번에 처음 확인했습니다. birchholt.pages.dev 홈에서는 x-robots-tag 줄이 하나, 값도 noindex 하나였습니다.

중요한 건 아래 다섯 줄입니다. 위쪽은 규칙이 제 역할을 했는지 보는 것이고 아래쪽은 규칙이 엉뚱한 데까지 번지지 않았는지 보는 것입니다. birchholt.com에서 한 줄이라도 나오면 따질 것 없이 _headers를 빼고 다시 배포하기로 정해 두었는데, 그럴 일은 없었습니다.

본문은 지금도 두 주소가 같습니다. 홈의 md5를 다시 찍어보니 둘 다 c716d3650389a85afd5cd7ed4bd95958이었습니다. 처음 찍었을 때와 값이 다른 건 그 사이에 새로 배포했기 때문입니다. 내용은 그대로 두고 들어온 도메인에 따라 헤더만 달라진 겁니다.

2026-10-04 14:00 UTC에 찍었습니다. 사이트맵(sitemap.xml), robots.txt, CSS 파일도 pages.dev 쪽에만 noindex가 붙었고 birchholt.com 쪽에는 없었습니다.

robots.txt로 막으면 안 됩니다. pages.dev 쪽만 크롤을 막고 싶어도 robots.txt는 같은 파일이 두 도메인에 똑같이 나갑니다. 그리고 크롤을 막으면 크롤러가 noindex 헤더를 읽을 기회 자체가 사라집니다. Google 문서는 noindex가 효과를 내려면 「must not be blocked by a robots.txt file」이라고 적고 있습니다 — Google · Block Search indexing with noindex.

미리보기 배포 치우기

모래밭에 세워 둔 시험용 모래성을 밀물이 오기 전에 손으로 쓸어 평평하게 만드는 장면

실험이 끝나서 미리보기 배포를 API로 지웠습니다. 처음엔 거절당했습니다.

{
  "success": false,
  "errors": [{
    "code": 8000035,
    "message": "You cannot delete an aliased deployment without a `?force=true` parameter in the API request URL."
  }]
}

브랜치 이름 주소(noindex-test.birchholt.pages.dev)가 이 배포를 가리키고 있어서입니다. 요청 주소 끝에 ?force=true를 붙이니 "success": true가 돌아왔습니다.

Preview deployments 문서는 「The latest deployment for a branch cannot be deleted.」라고 적고 wrangler에서는 --force로 별칭 붙은 배포를 지우라고 안내합니다. 이번 배포는 noindex-test 브랜치의 유일한, 그러니까 가장 최근 배포였는데 API에 force=true를 주니 지워졌습니다(2026-10-04).

지우고 20초 뒤에 다시 찍었습니다. 브랜치 이름 주소는 404였는데 해시 주소는 아직 200으로 열려 있었습니다. 1분 남짓 지나 다시 찍었을 때 해시 주소도 404가 됐습니다. 그 사이 birchholt.com과 birchholt.pages.dev는 처음부터 끝까지 200이었고 헤더도 그대로였습니다.

남은 일은 기다리는 것입니다

씨앗을 심은 화단 옆 의자에 앉아 달력에 표시를 하나 해 두는 사람

헤더를 붙인다고 이미 검색 결과에 있던 주소가 그날 바로 빠지지는 않습니다. 크롤러가 그 주소를 다시 찾아와서 헤더를 읽어야 하고 그게 언제일지는 제가 정할 수 없습니다. 그래서 빠졌다는 결과는 이 글에 적지 않았습니다. 반영한 뒤 몇 주에 걸쳐 지켜볼 일입니다. 지켜보는 방법은 단순합니다. Google에서 site:birchholt.pages.dev로 검색해서 나오는 주소가 줄어드는지 봅니다.

더 확실한 방법도 있습니다. pages.dev 주소를 .com으로 아예 리다이렉트하는 겁니다. 다만 Pages의 _redirects 파일은 도메인 단위 리다이렉트를 지원하지 않고 Cloudflare는 이 경우에 Bulk Redirects라는 별도 기능을 쓰라고 안내합니다. 이번에는 리다이렉트는 시도하지 않고 헤더로만 막았습니다.

Cloudflare Pages · Redirects의 지원하지 않는 기능 표에 「Domain-level redirects ❌」, 그리고 Redirecting *.pages.dev to a Custom Domain이 Bulk Redirects로 하는 절차입니다. 2026-10-04에 확인했습니다.

커스텀 도메인을 붙여 쓰는 Pages 프로젝트라면 지금 한 번 찍어보세요.

curl -sI https://<프로젝트>.pages.dev/ | grep -i x-robots-tag

아무것도 안 나오면 그 주소는 열려 있는 겁니다. 제 경우 해시가 붙은 주소에는 noindex가 보였으니 그것만 찍어봤다면 프로젝트 주소도 막혀 있다고 착각했을 겁니다.