o2o-site-AEO/solution/backend/services/indexnow.py
Mina Choi 11d30bb3d1 [chore] solution,admin,ontology: 코드 주석을 한 줄로 — 히스토리 주석 삭제
여러 줄 주석이 설명보다 경위(예전·실측·지적)를 적고 있어 읽는 사람이 결론을 찾기 어려웠다.

- ts·tsx·js·mjs·css·py 478개: 여러 줄 주석은 첫 문장 한 줄로, 과거형·날짜 문장은 삭제
- 주석 위치는 TypeScript 파서·파이썬 tokenize/ast 로 찾는다 — 문자열 안의 # · /* 는 건드리지 않는다
- eslint·ts·noqa·type: ignore 같은 지시 주석은 그대로 둔다

파이썬 275개 정리 전후 AST 동일, TS 298개 주석 뺀 토큰 동일(빈 JSX 주석 10곳만 차이).
site·frontend·admin tsc, site vitest 105 passed

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:05:19 +09:00

89 lines
2.8 KiB
Python

"""발행본 URL 을 IndexNow 로 알린다 — 크롤러가 찾아올 때까지 기다리지 않는다."""
import os
import xml.etree.ElementTree as ET
from pathlib import Path
from urllib.parse import urlsplit
import httpx
from common.logger import LOG
ENDPOINT = "https://api.indexnow.org/indexnow"
SITEMAP_NS = "{http://www.sitemaps.org/schemas/sitemap/0.9}"
TIMEOUT_SEC = 10.0
# 규격 상한은 한 번에 10,000 개다.
MAX_URLS = 10_000
def key() -> str:
return os.environ.get("INDEXNOW_KEY", "").strip()
def is_configured() -> bool:
return bool(key())
def output_dir() -> Path:
return Path(os.environ.get("SITE_OUTPUT_DIR", "/app/solution/site/out"))
def site_urls(slug: str) -> list[str]:
"""이 사이트가 실제로 발행한 URL 목록(사이트맵의 `<loc>`)."""
sitemap = output_dir() / "s" / slug / "sitemap.xml"
if not sitemap.is_file():
return []
try:
root = ET.parse(sitemap).getroot()
except ET.ParseError as ex:
LOG.w(f"[indexnow] 사이트맵을 읽지 못했다 — {sitemap}: {ex}")
return []
urls = [(node.text or "").strip() for node in root.iter(f"{SITEMAP_NS}loc")]
return [url for url in urls if url][:MAX_URLS]
def _payload(urls: list[str]) -> dict | None:
"""IndexNow 요청 본문."""
host = urlsplit(urls[0]).netloc
if not host:
return None
same_host = [url for url in urls if urlsplit(url).netloc == host]
return {
"host": host,
"key": key(),
# 키 파일은 오리진 루트에 있다(프리렌더가 굽고 azure_static 이 올린다).
"keyLocation": f"https://{host}/{key()}.txt",
"urlList": same_host,
}
async def submit(slug: str) -> dict | None:
"""설정된 경우에만 통보한다."""
if not is_configured():
return None
urls = site_urls(slug)
if not urls:
LOG.w(f"[indexnow] 보낼 URL 이 없다 — 사이트맵이 없거나 비었다: {slug}")
return None
body = _payload(urls)
if body is None:
LOG.w(f"[indexnow] URL 에서 호스트를 못 읽었다: {urls[0]}")
return None
try:
async with httpx.AsyncClient(timeout=TIMEOUT_SEC) as client:
res = await client.post(ENDPOINT, json=body)
except httpx.HTTPError as ex:
LOG.w(f"[indexnow] 통보 실패 {slug}: {type(ex).__name__}: {ex}")
return {"ok": False, "error": f"{type(ex).__name__}: {ex}", "urls": len(body['urlList'])}
# 200 OK · 202 Accepted 가 정상이다.
ok = res.status_code in (200, 202)
if ok:
LOG.i(f"[indexnow] {slug} — URL {len(body['urlList'])}개 통보 (HTTP {res.status_code})")
else:
LOG.w(f"[indexnow] {slug} 거절됨 HTTP {res.status_code}: {res.text[:200]}")
return {"ok": ok, "status": res.status_code, "urls": len(body["urlList"])}