본문으로 바로가기

스캔 문제 — 문제 코드 레지스트리

Pro 스캔이 보고하는 모든 문제는 단일 레지스트리 Rankbeam\Seo\Pro\Scanning\IssueRegistry안정적인 문제 코드입니다. 스캐너는 즉석에서 코드를 만들지 않습니다. 각 문제를 IssueRegistry::make()로 구성하며, 레지스트리의 심각도와 필드를 지정하고 정의되지 않은 코드는 거부합니다. 따라서 아래 목록을 계약으로 삼아 구현할 수 있습니다. 대시보드, 내보내기, Pro 점수는 메시지를 파싱하지 않고 코드를 읽습니다. 무료 seo:audit는 더 좁은 범위와 일부 다른 hreflang 코드를 가진 자체 코어 메타데이터 레지스트리를 사용합니다.

각 코드에는 다음이 있습니다.

  • idseo_scan_issues.issue_type로 저장되는 안정적인 문자열.
  • severitycritical, warning, notice 중 하나이며 코드별로 고정됩니다. 심각도를 바꾸지 않고 코드를 나눕니다.
  • field — 해당 seo_meta 필드 또는 페이지 수준 발견 사항의 page.
  • execution class — 탐지에 필요한 실행 조건(아래 참고).
  • evidence — 문제의 context 배열에 포함된 키.

실행 클래스

검사는 필요한 실행 조건에 따라 정확히 세 클래스 중 하나에 속합니다.

클래스필요 조건실행 주체
metadata모델 + 코어 리졸버, 페이지 요청 없음모델 스캔(PageScanner); 무료 seo:audit는 일부 메타데이터 검사 지원
rendered실제 제공되는 페이지 HTML(프로세스 내 커널 요청 또는 외부 요청)URL 스캔(UrlScanner)
network다른 곳을 가리키는 표준 URL 같은 별도 대상을 검증할 외부 요청URL 스캔, 항상 SsrfGuard를 통과

무료 프로세스 내 감사가 전체 Pro 스캔과 같을 수 없는 이유입니다. 페이지 렌더링 없이 계산할 수 있는 것은 metadata 코드뿐이며, 렌더링된 HTML을 가져오고 네트워크로 표준 URL 대상을 검증하는 것은 Pro 파이프라인뿐입니다. IssueRegistry::byClass(IssueRegistry::EXEC_METADATA)로 클래스별 레지스트리를 필터링하세요.

메타데이터 코드

PageScanner가 모델과 리졸버에서 탐지합니다. missing_title, missing_description, 길이 코드들은 렌더링된 URL 스캔에서도 실제 <head>를 측정해 발생합니다. 코드는 같고 의미도 같습니다.

코드심각도필드증거의미
missing_titlecriticaltitle제목과 계산 가능한 대체값이 모두 없음.
missing_descriptionwarningdescription메타 설명과 계산 가능한 대체값이 모두 없음.
missing_og_imagenoticeog_imageOpen Graph 이미지와 계산 가능한 대체값이 모두 없음.
missing_focus_keywordnoticefocus_keywords포커스 키워드가 설정되지 않음.
duplicate_titlewarningtitletitle, duplicate_urls같은 로케일의 다른 페이지에서 제목 재사용.
duplicate_descriptionwarningdescriptiondescription, duplicate_urls같은 로케일의 다른 페이지에서 설명 재사용.
title_too_longwarningtitlelength, max, script최종 제목이 문자 체계의 권장 길이를 초과(라틴 60, CJK 약 30).
title_too_shortnoticetitlelength, min, script최종 제목이 문자 체계의 하한 미만(라틴 30, CJK 약 15).
description_too_longwarningdescriptionlength, max, script최종 설명이 문자 체계의 권장 길이를 초과(160 / 약 80).
description_too_shortnoticedescriptionlength, min, script최종 설명이 문자 체계의 하한 미만(70 / 약 35).
robots_conflict_indexingcriticalrobotsrobotsRobots 지시문에 indexnoindex가 함께 있음.
robots_conflict_followingwarningrobotsrobotsRobots 지시문에 follownofollow가 함께 있음.
noindex_warningwarningrobotsrobots, canonical, page_url, shipping_signal자기 참조 표준 URL 페이지가 noindex임. 검토할 휴리스틱이며 반드시 색인되어야 한다는 증거는 아님. 모델 및 렌더링된 URL 스캔 모두에서 발생.
invalid_canonicalcriticalcanonicalcanonical표준 URL 값이 유효한 URL이 아님.
cross_domain_canonicalwarningcanonicalcanonical, page_url표준 URL이 페이지와 다른 호스트를 가리킴.
shared_canonicalnoticecanonicalcanonical여러 페이지가 같은 표준 URL을 선언함.
insecure_canonicalwarningcanonicalcanonicalhttps 사이트의 http:// 표준 URL.
hreflang_invalid_codewarningalternatesinvalid_codeshreflang 대체 페이지 값이 x-default도 유효한 BCP-47 언어 코드도 아님.
hreflang_missing_self_referencewarningalternateslocale, page_url대체 페이지를 선언했지만 페이지 자신의 로케일을 참조하는 자기 참조 hreflang이 없음.
hreflang_duplicate_codewarningalternatesduplicate_codes같은 hreflang 코드가 여러 URL에 대응함(모호한 군집).
hreflang_missing_x_defaultnoticealternateslanguages다국어 hreflang 군집에 x-default 대체값이 없음.
aeo_missing_authornoticeschema구조화 데이터의 글에 작성자 엔터티가 없음(작성자 / 출처를 스키마에 명시하지 않음).
aeo_article_missing_datenoticeschema구조화 데이터의 글에 게시/수정 날짜가 없음(시간 정보를 스키마에 명시하지 않음).

길이 임계값은 코어의 문자 체계별 길이 정책에서 가져옵니다(Pro 2.33). 라틴 문자 텍스트는 60/160, CJK는 약 30/80이며 그래핌 클러스터(사용자가 한 글자로 인식하는 단위)로 세므로 편집기 글자 수 표시와 스캔이 어긋나지 않습니다. 하한(라틴 제목 30, 설명 70, CJK는 약 절반)은 최적화 부족을 표시하는 스캔 기준이며, script 문맥 키가 적용한 분류를 나타냅니다. 길이는 대체값과 제목 접미사를 포함해 실제 렌더링되는 최종 제목/설명을 측정합니다.

hreflang_* 코드는 리졸버의 alternates에서 읽은 페이지의 hreflang 선언을 검증합니다. 잘못되거나 중복된 코드, 자기 참조 누락, 다국어 군집의 x-default 누락을 검사합니다. 페이지가 대체 페이지를 선언할 때만 실행합니다. 페이지 간 상호 참조는 메타데이터 검사에서 확인하지 않습니다. 아래의 선택적 네트워크 검사가 다른 페이지를 가져옵니다.

aeo_* 코드는 답변 준비도(AEO) 신호로, 페이지의 글을 구조화 데이터에서 이해할 수 있는지 살핍니다. 최종 JSON-LD 그래프를 읽고 글 유형(Article, BlogPosting, NewsArticle 등)을 선언했지만 author 엔터티(명시적 작성자/출처) 또는 datePublished / dateModified(명시적 시간 정보)가 없을 때 발생합니다. 글이 없는 페이지에는 표시하지 않습니다. 기본 활성화된 seo-pro.scan.checks.aeo가 제어하며 무료 seo:audit와 대응합니다.

missing_focus_keyword에는 활성화 조건이 있습니다

포커스 키워드 알림은 코어의 포커스 키워드 흐름이 활성화되었을 때만 발생합니다(seo.keywords.enabled, 기본값 false). 꺼져 있으면 키워드가 없다고 표시하지 않습니다. 무료 seo:audit 명령과 Filament 편집기도 같은 코어 플래그를 읽으므로 스캔, 감사, 편집기 알림이 항상 일치합니다. 활성화 조건은 하나뿐입니다.

렌더링 코드

UrlScanner가 실제 제공 HTML에서 탐지합니다. 동일 호스트는 외부 트래픽 없는 프로세스 내 커널 요청, 외부 대상은 보호된 요청을 사용합니다.

코드심각도필드증거의미
http_errorcriticalpagestatusURL이 4xx/5xx 상태로 응답.
empty_responsecriticalpageURL이 빈 본문 반환.
missing_canonicalnoticecanonical렌더링된 head에 <link rel="canonical"> 없음.
noindex_pagenoticerobotsrobots렌더링된 페이지가 noindex임(정보). noindex이면서 자기 참조 표준 URL이면 대신 점수에 반영되는 noindex_warning 코드로 격상.
missing_h1noticepage<h1> 제목 없음.
multiple_h1noticepagecount<h1> 태그가 여러 개(정보).
missing_image_altwarningpagecount, total, sample콘텐츠 이미지에 alt 속성 없음(명시적 alt=""는 장식용으로 처리해 표시하지 않음).
thin_contentnoticepageword_count, threshold, segmenter본문이 설정 단어 수 미만. 체크리스트 토크나이저가 띄어쓰기 문자 체계는 공백, 중국어·일본어·태국어는 ICU 사전 분할(segmenter: intl, ext-intl 필요)로 세므로 400단어 일본어 글을 한 “단어”로 세지 않음.
mixed_contentwarningpagecount, samplehttps 페이지에 http:// 하위 자원.
html_lang_missingnoticepage<html lang> 속성이 없거나 비어 있음. 보조 기술이 부적절한 음성을 선택할 수 있음.
html_lang_invalidnoticepagedeclaredlang 값이 BCP-47 태그가 아님(english, 밑줄이 있는 en_US, jp).
html_lang_mismatchwarningpagedeclared, declared_script, detected_script보이는 본문의 문자 체계가 선언 언어와 다름(일본어 페이지의 lang="en", 라틴 문자 본문의 lang="ru"). 문자 체계 수준만 판단하며 라틴 언어 간 차이는 추측하지 않음. 본문에 글자 ≥ 40개 필요.

네트워크 코드

UrlScanner는 해당 선택적 플래그가 켜져 있을 때만 탐지합니다. 표준 URL 대상은 seo-pro.scan.url_checks.check_canonical_target, hreflang 대체 페이지는 check_hreflang_reciprocity입니다. 모든 대상은 스킴 허용 목록, 호스트 범위, 사설 IP 거부, 리디렉션/시간/크기 한도를 적용하는 SsrfGuard를 통해 가져옵니다. 리디렉션을 따라가지 않으므로 리디렉션하는 표준 URL을 확인할 수 있습니다. 페이지 자체를 방금 가져왔으므로 자기 참조 표준 URL이나 대체 페이지는 건너뜁니다.

코드심각도필드증거의미
blocked_urlnoticepagereasonHTTP 요청 전에 SsrfGuard가 대상을 거부.
canonical_target_brokencriticalcanonicalcanonical, status표준 URL 대상이 HTTP 오류 반환.
canonical_target_redirectwarningcanonicalcanonical, status, location표준 URL 대상이 리디렉션함. 최종 URL을 지정할 것.
canonical_target_noindexwarningcanonicalcanonical표준 URL 대상 자체가 noindex임.
canonical_target_blockednoticecanonicalcanonical, reason표준 URL 대상 검증 불가(가드 거부 / 해석 불가).
hreflang_not_reciprocalwarningalternateshreflang, href, status선언한 대체 페이지가 원래 페이지를 다시 선언하지 않음. hreflang 쌍이 무시될 수 있으나 이것만으로 번역 페이지의 색인이 불가능해지는 것은 아님.
hreflang_target_unverifiednoticealternateshreflang, href, reason대체 페이지 요청 불가(가드 거부, 오류 상태, 리디렉션, 크기 초과)로 상호 참조를 검사하지 못함. 결함이 아닌 증거 부족.

상호 참조는 페이지당 최대 hreflang_max_alternates개(기본 10) 대상을 요청하며 x-default를 포함하고 중복과 자기 자신은 건너뜁니다. 위 모델 수준 hreflang_* 메타데이터 코드는 선언된 목록을 검증합니다. 다른 페이지가 필요한 검사는 이 크롤링입니다.

모든 네트워크 경로는 공유 SsrfGuard를 재사용합니다. 위협 모델과 남아 있는 TOCTOU 주의 사항은 SECURITY.md를 참고하세요.

코드와 점수의 관계

Pro SEO 점수는 위 심각도에 따른 문제별 고정 감점을 합산해 100에서 뺀 값입니다(100 −). 대부분의 코드는 반영하지만 일부는 의도적으로 제외합니다. missing_focus_keyword(권고), noindex_pagemultiple_h1(정보), blocked_url / canonical_target_blocked / hreflang_target_unverified(검사하지 못함 ≠ 결함), hreflang_*, html_lang_*, aeo_*(현재 점수에서 제외한 권고 신호)입니다. 점수 페이지에 전체 허용 목록과 코드별 감점이 있습니다.

문제 수명 주기

문제는 존재하는 동안만 남는 행이 아니라 수명 주기를 가집니다. 스캔은 대상 문제를 지우고 재생성하지 않고 상태를 조정합니다. 각 문제의 안정적인 식별자는 대상(모델은 scannable_type + scannable_id, 라우트/사이트맵 대상은 url)과 issue_type의 조합입니다. 코드 하나는 스캔당 대상별로 최대 한 번 발생합니다. “위반 항목 N개” 코드(missing_image_alt, mixed_content, hreflang_* 등)는 사례를 count / sample 값이 있는 행 하나로 합치므로 식별자가 고유합니다.

각 스캔에서 대상별로 다음을 수행합니다.

  • 기존 행이 없는 발견 사항은 open 상태로 생성하고 detected_at 값을 기록합니다.
  • 기존 미해결 행과 일치하는 발견 사항은 증거를 갱신하되 원래 detected_at 값을 유지합니다. 안정적인 최초 발견 시점이므로 매 스캔마다 초기화하지 않습니다.
  • 완료된 검사에서 더 이상 발견되지 않는 미해결 문제는 **fixed**로 표시하고 resolved_at 값을 기록합니다. 실제 해결을 남기도록 행을 삭제하지 않고 보존합니다.
  • fixed 문제가 재발하면 같은 행을 다시 열고 detected_at 값을 다시 기록합니다.
  • 사용자가 대시보드에서 **ignored**로 표시한 문제는 변경하지 않습니다.
상태의미설정 주체
open현재 존재함.스캔(신규 또는 계속 발견)
fixed존재했으나 더 이상 발견되지 않음.다음 실행에서 발견되지 않으면 스캔이 자동 설정
ignored사용자가 숨김; 미해결 수와 점수에서 제외.대시보드의 무시 동작

이제 해결을 버리지 않고 기록하므로 화이트라벨 보고서는 보고서 간 스냅샷 차이 대신 기간 내 실제 해결/신규 수를 보여 줄 수 있습니다. 대시보드, seo-pro:scan-status 명령, 점수는 모두 open만 필터링하므로 저장된 fixed 행이 수치를 부풀리지 않습니다. 해결된 행은 해결한 실행에 연결되며 일반 스캔 실행 보존 기간에 따라 정리됩니다.

설정

php
// config/seo-pro.php → 'scan'
'url_checks' => [
    'enabled' => true,
    'crawl_external' => false,             // fetch external URL targets (guarded)
    'check_canonical_target' => false,     // EXEC_NETWORK canonical validation (guarded)
    'check_hreflang_reciprocity' => false, // EXEC_NETWORK hreflang link-back crawl (guarded)
    'hreflang_max_alternates' => 10,       // targets fetched per page by that crawl
],
'checks' => [
    'length' => true,            // title/description length (metadata + rendered)
    'rendered_content' => true,  // H1 / alt / thin content / mixed content / html lang
],
'content' => [
    'min_word_count' => 200,     // thin_content threshold
    'evidence_sample' => 5,      // max example URLs stored per issue
],

보호된 요청의 응답 크기 한도는 seo-pro.http.max_response_bytes(기본 2 MB)입니다. 프로세스 내 동일 호스트 스캔에는 크기 한도가 없습니다.

호환성 참고 (문제 코드 이름 변경)

이전의 robots_conflict 코드 하나는 두 심각도를 가졌습니다. 코드마다 정확히 하나의 심각도에 대응하도록 나눴습니다.

이전 코드새 코드심각도
robots_conflict (index + noindex)robots_conflict_indexingcritical
robots_conflict (follow + nofollow)robots_conflict_followingwarning

robots_conflict를 저장하거나 필터링했다면 두 새 코드로 갱신하세요.

rankbeam/laravel-seo는 MIT 라이선스로 배포됩니다.