WordPress 운영 / VERIFIED CASE

noindex 글이 sitemap에 남는 배포 불일치 잡기

비교용 HTML에는 noindex, follow가 있지만 sitemap fixture에는 같은 URL이 남아 있었다. 두 파일을 따로 검사하면 둘 다 정상처럼 보였지만, URL 집합을 합치자 noindex_url_in_sitemap 1건이 나왔다. sitemap에서 그 URL만 제외하자 같은 명령이 충돌 0건으로 통과했다. 이 비교를 배포 전에 다시 쓸 수 있도록 진단 스크립트와 fixture로 고정했다.

HTML의 robots 메타만 검사하면 페이지 자체의 지시는 확인하지만 sitemap 생성 결과는 놓친다. 반대로 sitemap 문법만 검사해도 URL의 indexability를 알 수 없다. 두 결과를 URL 단위로 함께 검사해야 이 모순을 찾을 수 있다. 이번 기록은 운영 장애를 재현한 것이 아니라 공개 서비스에 영향을 주지 않는 통제 비교다.

비교 대상

페이지 목록에는 두 URL이 있다.

  • /verified-guide/: indexable이며 canonical이 자기 자신을 가리킨다.
  • /private-report/: noindex, follow이며 검색용 sitemap에 없어야 한다.

변경 전 fixture에는 두 URL이 모두 들어 있고, 변경 후 fixture에서는 /private-report/만 빠진다. 페이지의 ID, URL과 canonical은 바꾸지 않았다.

변경 전: 두 개의 개별 검사는 통과처럼 보인다

noindex 메타만 찾으면 /private-report/는 올바르게 설정된 것처럼 보인다. sitemap XML만 파싱해도 문법 오류는 없다. 문제는 둘을 합쳤을 때 드러난다.

.venv/bin/python scripts/huntlab_wp_diagnostics.py indexability \
  --pages evidence/lab-fixtures/indexability-pages.json \
  --sitemap evidence/lab-fixtures/sitemap-before.xml

실행 결과는 종료 코드 1이다.

passed=false
reason=indexability_conflict
checked_pages=2
sitemap_urls=2
conflict=https://example.test/private-report/
conflict_reason=noindex_url_in_sitemap

이렇게 해야 CI나 Publisher가 모순을 경고만 남기고 성공 처리하지 않는다.

수정은 문서가 아니라 sitemap 집합에 적용했다

해결 후 fixture는 indexable URL 하나만 포함한다. 같은 페이지 판정 파일과 같은 명령을 사용했다.

.venv/bin/python scripts/huntlab_wp_diagnostics.py indexability \
  --pages evidence/lab-fixtures/indexability-pages.json \
  --sitemap evidence/lab-fixtures/sitemap-after.xml
passed=true
reason=consistent
checked_pages=2
sitemap_urls=1
conflicts=[]

수정 전에는 충돌 1건, 수정 후에는 0건이다. 네트워크 쓰기와 WordPress 쓰기는 모두 0회였다. 테스트한 입력과 실행기는 고정 커밋에 남겼다.

진단기가 확인하는 두 방향

검사는 noindex URL의 sitemap 포함만 찾지 않는다. HuntLab이 검색 대표 문서로 선정한 페이지라면 canonical이 자기 URL인지도 확인한다. 외부 URL이나 다른 내부 URL을 canonical로 가리키면서 sitemap에 포함된 경우 역시 이 도구의 대표 문서 목록 계약과 일치하지 않기 때문이다. 이 규칙은 모든 웹페이지가 반드시 self-canonical이어야 한다는 Google 정책이 아니다. 중복 문서가 다른 대표 URL을 가리키는 canonical은 정상적인 설계다. 그런 문서는 대표 문서 감사 목록에서 분리해야 한다.

if page["noindex"] and url in sitemap_urls:
    conflicts.append((url, "noindex_url_in_sitemap"))
if not page["noindex"] and page["canonical"] != url:
    conflicts.append((url, "indexable_non_self_canonical"))

전체 분기는 진단 코드, 실패 후 통과 조건은 회귀 테스트에 있다.

Google은 canonical과 sitemap을 대표 URL을 알리는 신호로 설명하며 필수 지정으로 요구하지 않는다. 이 글의 게이트를 검색엔진 색인 보장이나 AdSense 승인 조건으로 해석해서는 안 된다. Google의 중복 URL 통합 문서에서 각 신호의 의미를 확인할 수 있다.

적용 범위와 한계

이 도구는 제공된 페이지 목록과 sitemap snapshot의 일관성을 검사한다. 검색엔진이 URL을 실제로 색인했는지, robots.txt에 다른 차단이 있는지, 렌더링 뒤 noindex가 삽입되는지까지 대신 확인하지 않는다. 공개 배포에서는 HTTP 응답, 렌더링된 HTML, sitemap, Search Console 상태를 별도로 기록해야 한다.

sitemap index가 여러 child sitemap을 가리키는 사이트라면 모든 child URL을 먼저 펼쳐야 한다. 첫 sitemap만 검사하면 페이지네이션을 첫 응답만 읽는 것과 같은 누락이 생긴다.

실제 사이트 입력을 만들 때 확인할 순서

  1. indexable/noindex 판정과 canonical을 URL별 JSON으로 저장한다.
  2. sitemap index와 모든 child sitemap의 URL을 합친다.
  3. noindex URL과 sitemap URL의 교집합이 0인지 확인한다.
  4. indexable URL의 canonical이 자기 자신인지 확인한다.
  5. 배포 후 공개 HTML과 sitemap을 다시 받아 같은 검사를 반복한다.

복사해서 실행하기

앞의 실행 준비를 마친 프로젝트 루트에서 다음 명령을 실행한다. 결과 파일 덮어쓰기를 막는 실행기이므로 매번 새 임시 디렉터리를 만들고, 출력된 경로에서 전후 결과와 회귀 테스트를 확인한다.

huntlab_result_dir=$(mktemp -d)
.venv/bin/python scripts/run_evidence_lab.py noindex-sitemap-consistency \
  --output "$huntlab_result_dir/result.json"
printf '결과 파일: %s/result.json\n' "$huntlab_result_dir"

결과 파일에서 before.exit_code=1, after.exit_code=0, status=READY, wordpress_writes=0을 확인한다. 실제 사이트에 적용할 때는 fixture 대신 공개 HTML과 모든 child sitemap에서 만든 snapshot을 입력해야 한다.

진단 스크립트와 fixture로 직접 재현하기