알림 접수와 검색 색인을 나눠 기록하기

새 글을 발행하거나 내용을 바꾼 사실을 검색엔진에 알리려고 Django에 IndexNow 제출 함수를 추가했다. 이전 글은 키 파일 배치부터 신호 연결까지 긴 코드를 실었지만, 실제 제출 조건과 실패 후 확인할 항목이 충분하지 않았다. 여기서는 현재 프로젝트가 보내는 요청과 아직 보완해야 할 부분을 구분한다.

IndexNow는 참여 검색엔진에 URL의 추가·수정·삭제를 알리는 프로토콜이다. 응답 200은 알림 접수, 202는 키 확인이 진행되는 접수 상태로 구별해서 해석한다. 어느 응답도 검색 결과에 이미 실렸다는 증거는 아니다. 지원 엔진과 상세 응답은 IndexNow 문서와 공식 FAQ에서 확인한다.

키 파일을 static에 넣는 것만으로 끝나지 않는다

수동 설정에서는 소유한 호스트에서 키 문자열을 담은 UTF-8 텍스트 파일을 공개한다. 파일 URL을 열었을 때 로그인 HTML이나 오류 페이지가 아니라 해당 문자열이 나와야 한다. 루트 이외 위치를 쓰면 keyLocation과 그 위치가 허용하는 URL 범위를 문서에 맞춰 확인한다.

INDEXNOW_HOST=example.com
INDEXNOW_KEY=example-key-20260922
INDEXNOW_KEY_LOCATION=https://example.com/example-key-20260922.txt

위 값은 구조를 설명하는 가상 예시다. 실제 운영 값은 서버 설정으로 관리한다. Django의 collectstatic은 정적 파일을 모으는 작업이므로, 그것만 실행했다고 /키.txt 경로가 생긴다고 생각하면 안 된다. Nginx 또는 Django URL 처리와 외부 GET 응답을 함께 확인해야 한다.

현재 제출 함수가 검사하는 조건

src/blog/indexnow.py의 submit_indexnow_urls()는 키·호스트·목록이 없으면 제출하지 않는다. 각 문자열의 앞뒤 공백을 제거하고 urlparse()로 나눈 뒤 다음 조건을 통과한 URL만 보낸다.

입력 예시, 설정 호스트는 example.com 현행 처리
https://example.com/new/ 전송 목록에 포함
http://example.com/new/ HTTPS가 아니므로 제외
https://other.example/new/ 호스트 불일치로 제외
https://example.com:443/new/ netloc가 설정 문자열과 달라 제외
숫자 또는 빈 문자열 유효한 제출 URL에서 제외

이것은 프로젝트의 검사 방식이지 IndexNow 규격 전체를 구현한 URL 검증기는 아니다. 중복 제거·대량 목록 분할·페이지 내용 검사까지 수행하지 않는다. 실제 요청은 https://api.indexnow.org/indexnow로 JSON을 보내며 timeout=8을 사용한다. Requests의 timeout은 프로그램 전체가 정확히 8초 안에 끝난다는 뜻으로 해석하지 않는다.

저장 신호의 현재 한계

글 저장 신호는 발행·공개 여부를 검사한 뒤 URL을 제출한다. 현재 신호는 글의 indexing_status를 검사하지 않는다. 따라서 공개 noindex 글도 일반 save() 경로에서는 알림 대상이 될 수 있다. 도구 저장 신호도 활성·공개와 언어 활성 설정을 사용하며 사이트맵의 검토 완료 조건을 그대로 재사용하지 않는다.

이 부분은 앞으로 제출 정책을 맞출 때 다뤄야 할 항목이다. 삭제 알림은 프로토콜이 지원하지만 현재 프로젝트의 post_save 신호만으로 삭제 이벤트가 자동 처리되지는 않는다. 또 QuerySet.update()를 사용하는 일괄 본문 보강 명령은 저장 신호를 발생시키지 않는다. 본문 반영 성공을 자동 제출 성공으로 기록하지 않는 이유다.

외부 요청 없이 조건을 시험하는 예시

다음 코드는 Django가 로드된 로컬 테스트 환경에서 실행하는 예시다. HTTP 전송 함수를 mock으로 바꾸므로 검색엔진에 실제 알림을 보내지 않는다.

from unittest.mock import Mock, patch
from django.test import override_settings
from blog.indexnow import submit_indexnow_urls

with override_settings(
    INDEXNOW_HOST="example.com",
    INDEXNOW_KEY="example-key-20260922",
    INDEXNOW_KEY_LOCATION="https://example.com/example-key-20260922.txt",
):
    with patch("blog.indexnow.requests.post", return_value=Mock(status_code=202)) as send:
        accepted = submit_indexnow_urls([
            "https://example.com/new/",
            "http://example.com/new/",
            "https://other.example/new/",
        ])
        assert accepted is True
        assert send.call_args.kwargs["json"]["urlList"] == ["https://example.com/new/"]

이 예제로 검증하는 것은 URL 선별과 202 응답 처리다. 운영 키의 유효성·키 파일 접근·실제 엔진 접수 여부는 검증하지 않는다. 현재 함수는 200과 202를 모두 True로 반환하므로 둘의 세부 상태가 필요하면 응답 기록 구조도 개선해야 한다.

실패를 만났을 때

400이면 요청 형식, 403이면 키 파일과 검증, 422이면 호스트·URL·키의 범위를 살펴본다. 429는 무작정 반복 호출하기보다 제출 빈도를 조절할 신호다. 네트워크 예외는 함수가 False로 반환한다. 재시도 대기열이나 성공 이력 저장은 현재 함수에 없다.

기존 코드에는 payload 전체를 기록하는 디버그 로그도 있어 로그 공유 범위를 주의해서 검토해야 한다. 다음 개선에서는 제출 대상 정책, 트랜잭션 완료 후 처리, 중복 억제와 결과 이력을 함께 설계할 수 있다. 지금의 동작 설명과 개선 제안을 구분해야 운영 중 원인을 추적할 수 있다.