커스텀 도메인을 붙여도 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가 보였으니 그것만 찍어봤다면 프로젝트 주소도 막혀 있다고 착각했을 겁니다.