birchholt 직접 해보고 남기는 기록

Hugo sitemap에서 페이지 하나를 뺐는데, 어떤 표기는 경고 없이 무시됐습니다

테스트용으로 만든 페이지를 sitemap(사이트의 주소 목록을 검색엔진에 알려주는 파일)에서 빼고 싶을 때가 있습니다. Hugo에는 이 용도의 프론트매터 키가 있습니다. sitemap 아래 disable: true를 넣는 것입니다.

정원 안내도에서 아직 공사 중인 작은 온실 하나에만 얇은 종이를 덧대 가리는 손

그런데 Hugo 포럼에는 이걸로 헤맨 질문이 2022년부터 2024년까지 여러 번 올라와 있습니다. 그중 Disable page from sitemap 글은 2026-10-06 기준 조회가 3,382회입니다. sitemap.disable 이 나오기 전인 2022년 글이라 답이 지금과 다릅니다. 옛 글을 따라 하면 지금은 맞지 않는 방법을 쓰게 됩니다.

그래서 제 블로그 저장소를 복사한 사본에 테스트 페이지를 여러 개 만들고 표기를 바꿔 가며 빌드했습니다. Hugo v0.165.0 extended이고 2026-10-06에 잰 값입니다.

맞게 넣으면 빠지고, v0.125.0 전에는 조용히 무시됩니다

같은 모양의 열쇠 두 개 중 새 열쇠만 자물쇠에 맞아 돌아가는 오래된 나무 문

가장 기본인 표기부터 봤습니다.

---
title: "a-lower"
sitemap:
  disable: true
---

빌드하면 이 페이지의 <loc>(sitemap에 적히는 주소 한 줄)가 사라집니다. 아무것도 안 넣은 대조 페이지는 그대로 남았습니다. 공식 설정 문서의 문장도 “Set to true in front matter to exclude the page.“입니다.

이 키는 v0.125.0에서 생겼습니다. 릴리스 노트에 “tpl/tplimpl: Optionally exclude content from sitemap"이라는 항목으로 올라 있습니다. 그 전 버전에서는 어떻게 되는지 v0.124.1을 받아서 페이지 두 개짜리 시험용 사이트를 두 버전으로 빌드해 봤습니다.

v0.124.1  →  /  /hidden/  /shown/     (hidden 이 남아 있음)
v0.165.0  →  /  /shown/

v0.124.1은 경고도 에러도 없이 키를 그냥 무시했습니다. 포럼의 2024년 질문도 원인이 이것이었습니다. 질문자가 쓰던 Ubuntu 패키지의 Hugo가 오래된 버전이었습니다. 안 빠지면 hugo version부터 봅니다.

틀려도 조용한 표기가 넷 있었습니다

여러 우편함 중 이름표 철자가 조금씩 다른 몇 개에만 편지가 그대로 쌓여 있는 시골 길가

다음은 표기를 조금씩 틀려 봤습니다. 페이지 하나에 변형 하나씩 넣고 한 번에 빌드했습니다.

프론트매터sitemap
sitemap: 아래 disable: true빠짐
Sitemap: 아래 Disable: true (대문자)빠짐
TOML [sitemap] 아래 disable = true빠짐
TOML 한 줄 sitemap.disable = true빠짐
YAML 한 줄 sitemap.disable: true남음
params: 아래 sitemap: 아래 disable: true남음
sitemap: 아래 disabled: true (오타)남음
sitemap_exclude: true (옛 글의 방법)남음

“남음"인 넷 모두 빌드 출력에 경고가 한 줄도 없었습니다.

헷갈리기 쉬운 건 점(.) 표기입니다. TOML에서 sitemap.disable은 “sitemap 표 안의 disable"이라는 뜻이라 먹힙니다. YAML에서는 sitemap.disable이라는 이름의 키 하나가 생길 뿐이라 Hugo가 쳐다보지 않습니다. 문서 예시를 TOML로 보고 YAML 파일에 그대로 옮기면 여기 걸립니다.

대소문자는 상관없었습니다. 포럼 답변(jmooring)도 “The keys are case-insensitive"라고 적고 있습니다. 다만 같은 답에서 문서에 적힌 모양 그대로 쓰는 습관을 들이라고 덧붙였습니다.

sitemap_exclude는 2022년 포럼 답변에 나오는 방법입니다. 그때는 내장 템플릿을 복사해서 이 값을 거르는 조건을 직접 넣는 것까지가 한 세트였습니다. 키만 따라 넣으면 지금의 내장 템플릿은 그 키를 모릅니다.

값의 모양도 몇 가지 봤습니다.

disable: "true"   → 빠짐
disable: 1        → 빠짐
disable: "false"  → 남음
disable: yes      → 빌드 실패

yes만 빌드를 멈췄습니다. 에러 메시지는 이렇습니다.

failed to decode sitemap config in front matter: 1 error(s) decoding:
* cannot parse 'Disable' as bool: strconv.ParseBool: parsing "yes": invalid syntax

에러가 나는 쪽이 오히려 친절합니다. 무시되는 표기는 빌드가 성공하기 때문에 sitemap을 직접 열어 보기 전에는 알 길이 없습니다.

layouts/sitemap.xml 을 두면 disable 이 다시 꺼집니다

새로 그린 지도에 원래 지도에 있던 점선 울타리만 빠져 길이 다시 이어져 보이는 탁자 위 두 장의 지도

표기가 맞는데도 안 빠지는 경우가 하나 더 있습니다. sitemap 템플릿을 직접 만들어 둔 경우입니다.

Hugo가 들고 있는 내장 템플릿은 이렇게 페이지를 거릅니다.

{{ range where .Pages "Sitemap.Disable" "ne" true }}
  {{- if .Permalink -}}

disable은 이 where 한 줄이 있어서 먹히는 것입니다 (v0.165.0 내장 템플릿). 설정이 알아서 페이지를 빼 주는 게 아니라 템플릿이 그 값을 읽어서 빼는 구조입니다.

그런데 Hugo 문서의 Sitemap 메서드 예제에 실린 “simplistic sitemap template"은 {{ range .Pages }}로 시작합니다. 거르는 조건이 없습니다. 템플릿 문서의 덮어쓰기 안내도 .Sitemap.ChangeFreq와 .Sitemap.Priority만 언급합니다.

이 예제를 그대로 layouts/sitemap.xml에 두고 빌드해 봤습니다.

내장 템플릿          <loc> 31개
문서 예제 템플릿      <loc> 42개   (경고 0줄)

disable과 cascade로 뺐던 페이지 10개가 돌아왔고 하나가 더 붙었습니다. 붙은 하나는 내용이 빈 <loc></loc>입니다. build.render: never로 HTML을 안 만들게 한 페이지는 주소가 없는데, 내장 템플릿의 if .Permalink 검사가 빠져서 빈 줄이 나간 것입니다. 그 페이지 파일을 빼고 다시 빌드하니 빈 <loc>는 0개가 됐습니다.

옛 위치인 layouts/_default/sitemap.xml에 둬도 v0.165.0은 그 파일을 그대로 썼습니다. 2022년 포럼 글의 마지막 답도 “layouts 폴더에 sitemap.xml이 있어서 방해했다"였습니다. 테마나 예전 작업에서 넘어온 sitemap 템플릿이 있는지부터 찾아봅니다.

$ find layouts themes -name 'sitemap*.xml' 2>/dev/null

직접 템플릿이 필요하다면 range 줄을 내장 템플릿과 같이 where .Pages "Sitemap.Disable" "ne" true로 두고 if .Permalink 검사도 살립니다.

섹션째 빼기: _index.md 와 cascade 는 결과가 다릅니다

덩굴 시렁 맨 위에만 천을 덮었는데 아래 가지들은 그대로 드러난 모습과, 천이 아래까지 흘러내린 옆 시렁

테스트 페이지가 여러 개면 폴더(섹션)째 빼고 싶어집니다. _index.md에만 넣는 방법과 cascade로 넣는 방법을 비교했습니다.

1. 섹션의 _index.md에 sitemap.disable만 넣기

/sec-plain/          빠짐
/sec-plain/child/    남음

_index.md의 설정은 그 목록 페이지 자신에게만 붙습니다. 아래 글에는 안 내려갑니다.

2. _index.md에 cascade(아래 페이지로 값을 내려보내는 설정)로 넣기

cascade:
  sitemap:
    disable: true
/sec-cascade/          빠짐
/sec-cascade/child/    빠짐
/sec-cascade/keep/     남음   ← 이 글에만 disable: false

cascade 문서는 값을 “descendant pages"에 적용한다고 적고 있는데, 재 보니 섹션 목록 페이지 자신도 빠졌습니다. 목록은 남기고 글만 빼려면 target으로 종류를 좁힙니다.

cascade:
  - sitemap:
      disable: true
    target:
      kind: page

이렇게 하니 섹션 주소는 남고 아래 글만 빠졌습니다.

여기서 하나 걸렸습니다. cascade로 섹션을 통째로 뺐는데 두 페이지가 남았습니다. 하나는 disable: false를 일부러 넣은 페이지라 맞습니다. 나머지 하나는 sitemap: 아래 우선순위 값 priority만 적어 둔 페이지였습니다.

/lab/m-priority-only/    남음   (sitemap: priority: 0.3 만 있음)

사이트 설정 파일의 [[cascade]]로 해도, _index.md의 cascade로 해도 같았습니다. cascade 문서에는 “Hugo does not cascade a value if the descendant already defines the field"라는 문장이 있습니다. 제가 잰 결과로는 sitemap 묶음을 통째로 하나의 필드로 보는 것 같습니다. 페이지에 sitemap: 아래 키가 하나라도 있으면 위에서 내려오는 disable이 안 들어옵니다. 오타였던 disabled: true 페이지가 cascade에서도 남은 것도 같은 이유로 보입니다.

대조로 사이트 설정의 [sitemap]에 disable = true를 넣어 봤습니다. 이쪽은 키 단위로 합쳐져서 priority만 있는 페이지도 빠졌습니다. 남은 건 disable: false를 명시한 두 페이지뿐이었습니다. “전부 빼고 명시한 페이지만 넣는” 허용 목록 방식으로도 쓸 수 있습니다.

sitemap에서 빠져도 페이지는 열리고 색인 막는 표시도 없습니다

지도에서 지워진 오솔길 끝에 작은 정자가 그대로 서 있고 산책하던 사람이 문을 열고 들어가는 숲

여기서 자주 하는 오해가 있습니다. sitemap에서 뺐다고 검색에 안 나오는 건 아닙니다.

빠진 페이지의 빌드 결과를 로컬 서버로 띄워 찍어 봤습니다.

$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18731/lab/a-lower/
200
$ curl -s http://127.0.0.1:18731/lab/a-lower/ | grep -oE '<meta name="robots"[^>]*>|<link rel="?canonical"?[^>]*>'
<link rel="canonical" href="https://birchholt.com/lab/a-lower/">

페이지는 200으로 열립니다. robots 메타도, X-Robots-Tag 헤더도 없습니다. canonical(이 주소가 원본이라는 표시)은 자기 자신을 가리킵니다. 사이트 RSS(index.xml)에도 그대로 실려 있었습니다. sitemap 한 곳에서만 빠진 것입니다.

Google 문서는 sitemap을 “which pages and files you think are important"를 알리는 파일이라고 적고 크롤러는 “URLs found in previously crawled pages"를 따라 다닌다고 적고 있습니다 (Sitemaps overview). 색인 자체를 막는 건 noindex입니다. “noindex is a rule set with either a <meta> tag or HTTP response header and is used to prevent indexing content” (Block indexing).

그러니 목적이 “목록에서 덜 알리기"면 sitemap.disable, “검색에 안 나오게"면 noindex입니다. 제 사이트에서도 noindex는 404 페이지, 옮긴 주소에 남긴 alias 페이지, 그리고 pages.dev 주소에 붙는 응답 헤더(_headers) 세 곳에서 따로 붙이고 있고 sitemap 설정과는 엮여 있지 않습니다. 헤더(X-Robots-Tag)로 붙인 과정은 pages.dev 주소를 색인에서 뺀 글에 적었습니다. noindex가 먹으려면 그 페이지가 robots.txt로 막혀 있지 않고 크롤러가 접근할 수 있어야 한다는 점도 같은 Google 문서에 있습니다.

빼고 나서 sitemap을 직접 세어 보는 법

바구니에서 꺼낸 사과를 하나씩 세며 목록표와 맞춰 보는 과수원 창고의 작업대

결국 확인은 빌드 결과의 sitemap을 직접 열어 보는 것뿐입니다. 저는 이 두 줄로 봅니다.

$ hugo --quiet -d /tmp/out
$ grep -o '<loc>[^<]*' /tmp/out/sitemap.xml

grep -c '<loc>'로 세면 안 됩니다. --minify로 빌드한 sitemap은 한 줄로 붙어 나옵니다. 제 라이브 sitemap을 받아 세 보니 grep -c는 1, grep -o '<loc>' | wc -l은 21이었습니다.

안 빠졌을 때는 이 순서로 봤습니다.

  1. hugo version이 v0.125.0 이상인지. 아니면 키가 조용히 무시됩니다.
  2. layouts/나 테마에 sitemap.xml이 있는지. 있으면 그 안에 Sitemap.Disable 거르기가 있는지 봅니다.
  3. 표기가 맞는지. YAML 점 표기, params 아래, 오타는 전부 경고 없이 넘어갑니다.
  4. cascade로 뺐는데 남은 페이지가 있으면 그 페이지에 sitemap: 아래 다른 키가 있는지 봅니다.
  5. 검색에서도 숨겨야 하면 sitemap과 별개로 noindex를 붙입니다.

sitemap의 lastmod가 안 바뀌는 문제는 축이 다른 이야기라 따로 적었습니다.