링크 점검기가 /cdn-cgi/l/email-protection 을 404로 잡는데, 고장 난 건 없었습니다
Cloudflare를 앞에 둔 블로그에서 링크 점검 도구를 돌리면 이런 주소가 404로 잡힐 때가 있습니다.
/cdn-cgi/l/email-protection#8ce1e3… 처럼 뒤에 긴 16진수가 붙은 주소입니다.
제가 만든 적 없는 경로라 처음 보면 어디가 깨졌는지부터 찾게 됩니다.

제 블로그도 9월 중순까지 그랬습니다.
2026-09-19 기록에는 푸터의 메일 주소가 JS를 끄면 [email protected]로 보인다고 적혀 있습니다.
9월 22일에 대시보드에서 기능을 껐고 지금은 평문 mailto가 나갑니다.
다만 끈 것만으로는 다시 켜졌을 때 무엇이 바뀌는지 모릅니다.
그래서 그걸 재 보고 다시 켜져도 메일 주소만은 그대로 나가게 하는 주석을 넣어 그 주석이 운영 HTML까지 살아남는지 확인했습니다.
2026-10-07, Hugo v0.165.0 extended, lychee 0.24.2 기준입니다.
그 404는 Cloudflare가 일부러 내는 페이지입니다

먼저 지금 제 사이트에서 그 경로를 직접 찍었습니다. 기능이 꺼져 있는데도 경로는 살아 있습니다.
$ curl -s -o /dev/null -w '%{http_code} %{size_download}\n' https://birchholt.com/cdn-cgi/l/email-protection
404 4718
$ curl -s -o /dev/null -w '%{http_code} %{size_download}\n' https://birchholt.com/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js
200 1239
/cdn-cgi/는 도메인을 Cloudflare에 연결하면 Cloudflare가 붙여 두는 자기 전용 경로입니다(Cloudflare 문서).
제 저장소에는 이런 파일이 없습니다.
4,718바이트짜리 404 본문을 열어 보면 제목이 Email Protection | Cloudflare이고 이런 문장이 있습니다.
You are unable to access this email address birchholt.com
… You must enable Javascript in your browser in order to decode the e-mail address.
<meta name="robots" content="noindex, nofollow">(검색엔진에 색인도, 링크 따라가기도 하지 말라는 표시)도 붙어 있습니다.
사이트가 깨진 게 아니라 Cloudflare가 “JS 없이는 주소를 못 보여 준다"고 안내하면서 상태 번호를 404로 내는 것입니다.
난독화가 켜진 다른 공개 사이트의 같은 경로도 404였습니다.
켜지면 HTML이 이렇게 바뀝니다

제 사이트는 지금 꺼져 있어서 켜진 모습을 보려고 난독화가 켜진 공개 사이트 두 곳을 curl로 받아 봤습니다.
읽기만 했고 사이트 이름은 뺐습니다. 주소는 예시로 바꿨습니다.
<!-- 원래 블로그가 내보낸 것 -->
<a href="mailto:me@example.com">me@example.com</a>
<!-- 방문자가 받는 것 -->
<a href="/cdn-cgi/l/email-protection#88fbfd…"><span class="__cf_email__" data-cfemail="d3a0a6…">[email protected]</span></a>
<script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js"></script>
mailto 링크는 email-protection 링크로, 화면 글자는 [email protected]로 바뀌고 해독 스크립트가 하나 붙습니다.
같은 페이지를 두 번 받으니 data-cfemail 값이 d3a0…에서 3c4f…로 달라졌습니다.
응답할 때마다 값이 바뀝니다.
제 사이트가 켜져 있던 9월에도 같은 주소를 두 번 받아 비교하니 그 한 줄만 달랐다는 기록이 남아 있습니다.
HTML을 해시로 비교해 변경을 감지하는 도구라면 매번 바뀌었다고 판단할 겁니다.
User-Agent(요청을 보낸 프로그램 이름)를 Googlebot 문자열로 바꿔도 두 사이트 모두 똑같이 난독화된 HTML을 줬습니다. 다만 구글 IP에서 보낸 요청이 아니라 이름만 흉내 낸 것이라 진짜 Googlebot이 받는 응답까지는 확인하지 못했습니다.
Cloudflare 문서는 해독 스크립트를 「defer attribute」로 넣는다고 적고 있습니다.
제가 받은 두 사이트의 <script> 태그에는 defer가 없고 data-cfasync="false"만 있었습니다(2026-10-07).
문서에는 가입하면 이 기능이 자동으로 켜진다는 문장(「Cloudflare enables email address obfuscation automatically when you sign up」)도 있습니다.
바이트가 얼마나 느는지는 제 빌드 결과물에 같은 모양의 변환을 그대로 적용해서 쟀습니다. 켜진 사이트에서 본 마크업을 흉내 낸 것이라 실제 Cloudflare 출력과 몇 바이트 다를 수 있습니다.
홈 (주소 1곳) 7,713 → 7,958 바이트 (+245)
개인정보처리방침 (2곳) 6,602 → 6,984 바이트 (+382)
+ 첫 방문 때 해독 스크립트 1,239 바이트 요청 1회 (라이브에서 잰 크기)
JS를 끈 방문자에게는 주소가 사라집니다

해독 스크립트는 제 사이트에서 받은 원본(1,239바이트)을 그대로 썼습니다.
코드를 읽어 보면 일은 단순합니다.
앞 두 자리 16진수가 열쇠이고, 나머지를 그 열쇠로 XOR(비트를 뒤집는 간단한 연산)해서 주소를 되돌립니다.
그리고 링크를 mailto:로 되돌린 다음 자기 <script> 태그를 페이지에서 지웁니다.
변환한 빌드를 로컬 서버로 띄우고 Playwright(브라우저를 코드로 조작하는 도구)로 JS를 켜고 끄며 푸터 링크를 읽었습니다. 결과를 보기 좋게 줄였고 주소는 예시로 바꿨습니다.
JS 켬 href=mailto:me@example.com 글자=me@example.com 해독 스크립트 태그: 없음
JS 끔 href=/cdn-cgi/l/email-protection#8ce1e3… 글자=[email protected] 해독 스크립트 태그: 있음
JS를 켠 쪽에서는 방문자 눈에 아무 차이가 없습니다. 스크립트가 일을 마치고 자기 태그까지 지웠기 때문입니다. JS를 끈 쪽은 주소가 화면에서 아예 사라지고 링크를 누르면 앞에서 본 404 안내 페이지로 갑니다. 연락처가 메일 하나뿐인 블로그라면 그 방문자에게는 연락할 길이 없는 셈입니다.
링크 점검기는 페이지마다 다른 404로 셉니다

lychee(사이트 링크가 살아 있는지 확인하는 명령줄 도구)를 두 빌드에 돌렸습니다.
--base-url을 라이브 주소로 줘서 /cdn-cgi/… 링크는 실제 사이트에 요청이 가게 했습니다.
$ lychee --base-url https://birchholt.com/ out-obf/index.html out-obf/privacy/index.html
[out-obf/index.html]:
[404] https://birchholt.com/cdn-cgi/l/email-protection#8ce1e3… | Rejected status code: 404 Not Found
[out-obf/privacy/index.html]:
[404] https://birchholt.com/cdn-cgi/l/email-protection#2d4042… | Rejected status code: 404 Not Found
[404] https://birchholt.com/cdn-cgi/l/email-protection#4f2220… | Rejected status code: 404 Not Found
(이번 주제와 무관한 외부 링크의 429 한 줄은 뺐습니다)
평문 빌드에서는 같은 자리에 오류가 없었습니다.
# 뒤 값이 매번 달라서 같은 메일 링크가 페이지마다 서로 다른 URL로 셉니다.
제 사이트는 HTML 24개 파일에 메일 링크가 26개라 켜진 상태로 전체를 돌리면 서로 다른 404 URL 26개가 잡힐 것으로 봅니다(전체 실행은 하지 않았습니다).
점검기에서 빼려면 --exclude를 주면 됩니다. 그런데 여기서 한 번 막혔습니다.
# cdn-cgi 링크를 --include 로 지정해 둔 채 --exclude 를 더했을 때 → 404 3건 그대로
$ lychee --base-url https://birchholt.com/ --include 'birchholt\.com/cdn-cgi|mailto' --include-mail --exclude cdn-cgi/l/email-protection …
# --include 를 빼면 → 0 Errors
$ lychee --base-url https://birchholt.com/ --exclude cdn-cgi/l/email-protection --exclude aboutads …
🔍 34 Total 🔗 25 Unique ✅ 30 OK 🚫 0 Errors 👻 4 Excluded
lychee 0.24.2에서는 --include에 걸린 URL이 --exclude보다 우선했습니다.
lychee --help에도 --include를 「Has preference over all excludes」라고 적어 두었습니다.
설정 어딘가에 /cdn-cgi/ 링크를 잡는 --include가 있다면 --exclude를 더해도 경고가 안 사라집니다.
(두 번째 명령의 --exclude aboutads는 이번 주제와 무관하게 429를 돌려준 외부 링크를 같이 뺀 것입니다.)
이 404가 진짜 끊긴 링크인지 헷갈린다면 없는 주소를 대조군으로 같이 찍던 방법이 여기서도 통합니다.
/cdn-cgi/l/email-protection은 기능을 꺼도 404로 남아 있으니 그 경로의 상태 번호만으로는 켜졌는지 꺼졌는지 알 수 없습니다.
보아야 할 것은 내 HTML에 그 링크가 들어 있는지입니다.
$ curl -s https://birchholt.com/ | grep -c 'email-protection'
0
메일 주소만 빼려는 주석이 두 번 지워집니다

기능을 통째로 끄지 않고 특정 주소만 빼는 방법은 Cloudflare 문서에 있습니다.
주소를 <!--email_off-->와 <!--/email_off--> 주석으로 감싸면 그 안은 건드리지 않는다고 합니다.
존 전체가 아니라 특정 호스트 같은 조건에 맞는 요청만 끄려면 Configuration Rules(조건에 맞는 요청에만 설정을 바꾸는 규칙)에 「Email Obfuscation」 항목이 있습니다(설정 목록).
주석 쪽이 간단해 보여서 Hugo 템플릿에 넣었는데 빌드 결과물에 주석이 없었습니다.
| 넣은 방법 | --minify | 결과물에 남은 email_off |
|---|---|---|
템플릿에 <!--email_off-->를 그대로 | 끔 | 0 |
{{ "<!--email_off-->" | safeHTML }} | 끔 | 남음 |
{{ "<!--email_off-->" | safeHTML }} | 켬 | 0 |
위 + keepComments = true | 켬 | 남음 |
첫 번째로 지우는 것은 Go 템플릿 엔진입니다.
Hugo 템플릿은 Go의 html/template으로 처리되는데, 템플릿에 적은 HTML 주석은 출력에서 빠집니다.
Go 문서에 「Comments are stripped from output, except for those passed in via the HTML, CSS, and JS types」라고 적혀 있습니다.
그래서 safeHTML로 “이건 HTML 값"이라고 넘겨야 남습니다.
두 번째는 minify(공백·주석을 걷어 파일을 줄이는 처리)입니다.
제 빌드는 hugo --gc --minify로 도는데, Hugo의 HTML minify 기본값이 keepComments = false입니다(Hugo 설정 문서).
그래서 설정을 하나 더 넣었습니다.
[minify.tdewolff.html]
keepComments = true
이러면 모든 주석이 살아남습니다.
제 사이트에서 이걸로 새로 살아난 주석은 email_off 쌍 말고는 글 본문에 있던 `
` 하나뿐이었습니다. 템플릿 주석은 어차피 Go가 먼저 지우니 늘어날 게 별로 없습니다.
메일 주소가 나오는 곳은 푸터, 404 페이지 본문, 개인정보처리방침 본문 세 곳이었습니다. 방침은 마크다운 글이라 주석을 본문에 그대로 적었습니다. 제 설정은 마크다운 안의 HTML을 그대로 통과시키도록 되어 있어서 됩니다. 세 곳을 고쳐 빌드한 결과입니다.
주소가 든 HTML 파일 24
<!--email_off--> 26 (닫는 주석도 26)
mailto: 26
email_off 밖에 남은 주소 0
홈 크기 7,713 → 7,746 바이트 (+33)
여기까지는 제 빌드에서 확인한 것입니다.
Cloudflare가 minify된 HTML(속성 따옴표가 빠진 <a href=mailto:…>) 안에서도 이 주석을 지키는지는 기능을 다시 켜야 알 수 있습니다.
이 수정을 운영 사이트에 반영한 뒤, 존 설정은 꺼 둔 채로 홈·방침·없는 주소(404) 순서로 다시 받아 봤습니다.
$ for p in / /privacy/ /404-check-zz/; do curl -s https://birchholt.com$p | grep -o '<!--email_off-->' | wc -l | tr -d ' '; done
1
2
2
$ curl -s https://birchholt.com/ | grep -c 'email-protection'
0
홈 푸터는 <!--email_off--><a href=mailto:…>…</a><!--/email_off--> 모양으로 나갑니다. 따옴표는 빠졌지만 주석은 남았습니다.
사이트맵의 21개 주소와 404 페이지까지 22곳을 훑으니 주석 24쌍, mailto: 24개였고 주석 밖에 남은 메일 링크는 0개였습니다.
다만 존 설정을 실제로 켜서 이 주석이 먹는지는 재 보지 않았습니다. 존 전체 설정을 바꾸는 실험이라 이번에는 하지 않았습니다.
앞의 에뮬레이션은 켜진 사이트 마크업과 실제 해독 스크립트로 “켜지면 어떻게 바뀌는지"까지만 보여 줍니다. Cloudflare가 주석을 보고 건너뛰는 동작은 흉내 내지 않았습니다.
확인하려면 몇 분만 켜 두고 curl -s https://birchholt.com/ | grep -c 'email-protection'이 0인지, mailto:가 평문 그대로인지, 출력에 주석이 남는지 본 뒤 다시 끄면 됩니다.
다시 켜져도 괜찮게 해 두는 순서

저는 존 설정을 끈 채로 두고 주석은 방어용으로 넣는 쪽을 골랐습니다. 문서대로라면 이 기능은 가입할 때 자동으로 켜지고 대시보드에서 다시 켜면 제 저장소를 하나도 고치지 않아도 방문자가 받는 HTML이 바뀝니다. 다시 고른다면 이 순서로 보겠습니다.
- 내 HTML에
email-protection이 있는지부터 봅니다. 경로의 404만 보면 켜졌는지 꺼졌는지 알 수 없습니다. 기능이 꺼져 있어도 그 경로는 404를 냅니다. - 연락처가 메일뿐이라면 JS를 끈 방문자에게 주소가 사라진다는 점을 따져 봅니다. 수집 봇을 막는 대가가 그 방문자입니다.
- 메일 주소만 빼려면
<!--email_off-->를 쓰되, Hugo라면safeHTML과keepComments = true둘 다 있어야 결과물에 남습니다. 넣은 뒤 빌드 결과물에서 개수를 세 봅니다. 켜진 상태에서 실제로 먹는지는 잠깐 켜서 따로 확인해야 합니다. - 호스트 같은 조건 단위로 끄고 싶으면 Configuration Rules의 Email Obfuscation 항목을 씁니다.
- 링크 점검기에서는
cdn-cgi/l/email-protection을 제외하되, lychee라면--include가--exclude를 이긴다는 걸 기억해 둡니다. - HTML을 해시로 비교해 변경을 감지하고 있다면, 켜진 동안에는 응답마다 한 줄이 달라진다는 걸 먼저 의심합니다.