AI GrowthAI 콘텐츠 제작 도구 모음

공식 웹사이트 GEO 개조 체크리스트: 6계층 정보 포인트와 실행 순서

제6장: 실전 심화편 제103문 ⏱ 약 15분

제6장: 실전 심화편 · 제103문

공식 웹사이트 문구 한 단락이 AI에 인용될 수 있는지 판단하는 테스트는 단 하나입니다:그것을 따로 발췌해 다른 사람의 답변에 붙여 넣고, 여전히 성립하는지 보는 것입니다.

성립하면 그것은 인용 가능한 단위입니다. 성립하지 않으면 그것은 그저 마케팅 문구일 뿐입니다. 모델은 당신의 페이지를 통째로 인용하지 않고, 문장을 발췌합니다. 따라서 개조의 착안점은 페이지가 아니라 단락과 필드에 있습니다.

아래는 「무엇을 할지, 어떤 순서로, 어떻게 검수할지」 세 단락으로 설명합니다. 바로 복사해 사용할 수 있는 도구는 모두 글 말미 부록에 모아 두었으며, 통째로 복사해 독립 체크리스트로 사용할 수 있습니다.

이 테스트를 구체적으로 하는 방법

바로 본 사이트로 시연합니다. 열어서 songbing.com.cn/100wen/를 보면서 대조하면 되고, 어떤 도구도 준비할 필요가 없습니다.

첫 번째 단계, 세 가지 질문을 작성합니다. 사용자가 어떻게 물을지 그대로 쓰고, 업계 용어로 바꾸지 마세요.

  1. 이 사이트는 무엇을 하는 곳인가요?
  2. 누구에게 적합한가요?
  3. 다른 SEO 튜토리얼과 비교하면 무엇이 다른가요?

두 번째 단계는 페이지에서 문장을 그대로 옮겨 적는 것입니다. 이 세 가지 질문에 이미 답하고 있는 문장을 찾아 한 글자도 바꾸지 말고 그대로 옮겨 적으세요. 이 홈페이지에서 여섯 개를 옮겨 적을 수 있습니다:

  1. 아이브로우와 제목: 중국어 자연 트래픽 실전 매뉴얼 · 중국어 비유료 마케팅 100문
  2. 부제목: 마케터, 창업자, PR 실무자를 위한 중국어 자연 트래픽 실전 가이드
  3. 설명 행: 질문당 800-1500자, 한 질문으로 한 가지 지식 포인트를 완전히 설명, 독립적으로 읽을 수 있음
  4. 데이터 행: 7대 장, 103개 문답, 편당 800+자, 매주 지속 업데이트
  5. 셀링 포인트: 전 세계 최초로 GEO/AEO를 체계적으로 다룬 중국어 서적, AI 추천을 받는 방법을 알려줌
  6. 셀링 포인트: 전 플랫폼 커버, Baidu + Douyin + Xiaohongshu + Zhihu + WeChat 搜一搜

세 번째 단계는 질문과 문장을 이어서 읽어 보며, 독자가 추가로 질문할지 확인하는 것입니다.

사용자 질문 페이지의 원문 독자의 첫 반응
이 사이트는 무엇을 하는 곳인가 중국어 자연 트래픽 실전 매뉴얼 · 중국어 비유료 마케팅 100문 반쯤 이해. 매뉴얼이라는 건 알겠지만 「비유료 마케팅」은 한 번 더 생각해야 함
이 사이트는 무엇을 하는 곳인가 마케터, 창업자, PR 실무자를 위한 중국어 organic traffic 실전 가이드 이해함. 합격
누구에게 적합한가 문항당 800-1500자, 한 문항으로 하나의 지식 포인트를 완전히 설명, 독립적으로 읽기 가능 읽는 법은 알지만 누구에게 쓰는지 모른다. 동문서답
읽을 가치가 있나 7대 챕터, 103개 문답, 편당 800+ 자, 매주 지속 업데이트 규모를 안다. 합격
다른 튜토리얼과 비교하면 전체 네트워크 최초로 GEO/AEO를 체계적으로 다룬 중국어 도서, AI 추천을 받는 방법을 알려드립니다 반신반의. 「전체 네트워크 최초」는 검증할 수 없다
다른 튜토리얼과 비교하면 Baidu + Douyin + Xiaohongshu + Zhihu + WeChat Search 커버리지를 안다. 합격

여섯 문장 중 네 문장은 쓸 수 있다. 이것이 실제 공식 웹사이트의 상황이다. 전부 맞지도, 전부 틀리지도 않고, 두세 군데에서 막힌다.

막힌 두 문장은 전체 단락을 다시 쓸 필요 없이 표현 하나만 바꾸면 된다:

원문: 전체 네트워크 최초로 GEO/AEO를 체계적으로 다룬 중국어 책

문제: 「전체 네트워크 최초」는 검증할 수 없어 AI가 전달할 수도, 독자가 대조할 수도 없다

수정: 제3장 총 15문항이 AI 검색을 전문적으로 다루며, 인용 메커니즘부터 llms.txt 설정까지 복사 가능한 템플릿이 있다

「비유료 마케팅」도 마찬가지다. 이 단어는 업계에서는 통하지만 처음 방문한 독자에게는 생소한 단어이므로, 앞에 설명 한 줄을 받쳐주면 충분하다:

원문: 중국어 비유료 마케팅 100문

수정: 중국어 비유료 마케팅 100문 —— 광고 없이 콘텐츠와 검색으로 자연 트래픽을 확보한다

판단 기준은 단 하나:독자가 이 문장을 가지고 스스로 판단할 수 있는가. 이 두 곳을 고치면 여섯 문장 모두 독립적으로 성립한다. 총 수정량은 짧은 두 문장이지, 한 번의 개편이 아니다.

마지막 결과를 미리 명확히 해두자: 만약 세 가지 질문이 페이지에서한 문장도 그대로 옮길 수 없다면, 더 이상 테스트하지 마라. 이 페이지는 답변 유닛이 부족하니 아래의 6계층 체크리스트에 따라 바로 보충하라.

1. 6계층 정보 포인트

모델이 하나의 질문에 답하려면 네 가지 종류의 원료가 필요합니다: 당신은 누구인지, 무엇을 할 수 있는지, 무엇으로 신뢰를 얻는지, 다른 사람과 어디서 차별화되는지. 공식 웹사이트에 어느 한 종류라도 빠지면 그 종류의 질문은 당신과 무관합니다.

계층 무엇을 해결하는가 빠지면 어떻게 되는가
1 엔티티 정체성 당신이 어느 회사인지 확인 동명 브랜드와 혼동되거나 아예 언급되지 않음
2 역량 경계 당신이 답인지 판단 막연하게만 언급되고 추천 자리에 들지 못함
3 권위 증거 당신을 쓸 용기가 있는지 판단 콘텐츠는 채택되지만 신뢰할 수 없음으로 표시됨
4 차별화 비교 선택 비교를 수행 선택 질문에서 당신의 자리가 없음
5 전환 경로 하나의 행동으로 귀결 언급되지만 리드가 발생하지 않음
6 기계 판독 가능 앞의 다섯 계층을 읽히게 하기 콘텐츠가 AI에게는 존재하지 않는 것과 같음

앞의 세 계층은 전제이고, 뒤의 세 계층은 증폭기입니다.

첫 번째 계층: 엔터티 정체성

보완할 정보: 회사 정식 명칭, 브랜드 약칭, 영문명, 이전 명칭 네 가지의 대응 관계; 설립 시기, 등록지, 통일사회신용코드; 주요 사업 한 문장 정의; 브랜드명과 제품명의 소속 관계; 명칭, 주소, 전화번호의 유일한 버전; 상공, 백과, 公众号을 가리키는 외부 아카이브 링크.

두 가지 요구 사항은 놓치기 쉽습니다. 첫째는전부 순수 텍스트로작성하는 것이며, 이미지나 차트로 만들지 마세요. 둘째는 네 곳의 표기가 글자 하나까지 일치해야 한다는 것입니다. 상공 정보, 백과 소개, 公众号 소개, 공식 웹사이트 푸터에 다르게 적혀 있으면 엔터티의 신뢰도가 낮아집니다.

이 계층은 가장 자주 건너뛰어지는데, 이유는 「이 상공 정보를 누가 보나」입니다.AI가 봅니다.

두 번째 계층: 역량 경계

각 사업마다 표준 답변 단락을 작성하며, 여섯 가지를 포함합니다: 정의 문장, 적용 대상, 적용되지 않는 상황, 산출물, 주기, 가격 책정 방식. 여기에 서비스 지역과 진입 조건, 제한과 전제 조건을 추가합니다.

「적용되지 않음」은 가장 직관에 반하며, 가장 가치 있습니다. 모델은 선형 문제에 답할 때 보통 추천 이유와 배제 이유를 동시에 제시해야 합니다. 사이트 전체에 배제 문장이 없으면 자신을 후보에서 배제하는 것과 같습니다. 「어떤 상황에서 우리가 최적해가 아닌지」를 명확히 쓰는 것이 오히려 추천 확률을 높입니다.

또한 가장 자주 건너뛰어지는 두 항목은 가격 책정과 제한 조건입니다. 구체적인 가격을 공개하지 않아도 되지만, AI가 말할 수 있는 것이 있어야 합니다. 그렇지 않으면 「상담이 필요합니다」라고만 답할 수 있습니다.

세 번째 계층: 권위 증거

보완할 것: 저자 서명과 저자 페이지, 자격 증서 번호와 발급 기관, 특허 번호, 검사 보고서 번호, 고객 사례의 공개 가능한 데이터, 데이터 출처, 페이지 업데이트 시간, 편집 및 사실 확인 정책, 면책 조항.

이 계층은 AI가 당신의 콘텐츠를 사용할 용기가 있는지를 결정합니다. 모든 숫자는 반드시출처, 기준, 시점세 가지를 갖춰야 합니다.

불합격: 전환율이 현저히 향상됨.

합격: 체험 전환율이 2.1%에서 3.4%로 상승(기준: 가입에서 결제까지, 14일 윈도우, 출처: 내부 트래킹, 2026년 3월).

선언에 관한 이 항목은 국내 사이트에서 거의 아무도 하지 않습니다. 하면 그것이 차별화입니다.

네 번째 계층: 차별화 비교

보완할 것: 비교표, 역량 매트릭스, 명확한 「우리가 하지 않는 것」.

비교 대상은 솔루션 유형으로 쓰고 경쟁사를 지목하지 않아, 법적 리스크를 피하면서도 사용자의 실제 비교 맥락을 포괄합니다. 비교에는 반드시 대가를 써야 하며, 장점만 말하는 비교는 마케팅 콘텐츠로 판정되어 답변에 들어가지 못합니다.

다섯 번째 계층: 전환 경로

수렴해야 하며, 늘리는 것이 아닙니다: 한 페이지에 주 CTA 하나만; 연락처는 전 채널에서 일관되고 복사 가능한 순수 텍스트 제공; 응답 약속 명시(얼마 만에, 누가 회신); 애프터서비스와 보장 조항 보완.

연락처를 아이콘이나 QR코드로만 만들면, AI가 전달할 수 있는 정보를 전혀 얻지 못하는 것과 같습니다.

여섯 번째 계층: 기계 판독 가능

우선순위에 따라 실행: 서버사이드 렌더링 또는 정적 생성; 시맨틱 HTML(페이지당 H1 하나, H2는 의문문, 본문은 main과 article 내부); 구조화 데이터; robots.txt로 AI 크롤러 용도 구분; llms.txt; sitemap 구역화; canonical과 hreflang 관리; 404와 리다이렉트 관리; 이미지 alt와 핵심 데이터 텍스트화.

단 하나만 할 수 있다면,서버사이드 렌더링을 하세요. 페이지 본문을 크롤링할 수 없으면 나머지 여덟 항목은 모두 무효가 됩니다.

구조화 데이터는 태그이지 콘텐츠가 아닙니다. 페이지에 없는 문장은 마크업에 써도 만들어낼 수 없습니다. 반대로 페이지에 존재하지 않는 정보를 마크업하는 것은 속임수이며, 모델의 교차 검증 시 역효과를 냅니다.

둘, 개조 순서

레벨 콘텐츠 기간
P0 SSR 또는 정적화; About Us에 실체 신원 단락 추가; 각 페이지에 한 문장 정의 추가; robots 및 AI 크롤러 전략; Organization 및 Article 마크업 2주
P1 Q&A 형식 소섹션 재구성; 저자 페이지, 자격 페이지, 사례 페이지; llms.txt; sitemap 및 hreflang 관리 1개월
P2 비교 매트릭스; 가격 정보; 모니터링 체계 지속

P0 내부에도 순서가 있다. 렌더링 방식을 바꾸지 않으면, 이후는 모두 보이지 않는 페이지에 인테리어를 하는 셈이다.

3. 검수

GEO는 백엔드 데이터가 없으므로 질문 시뮬레이션과 기록만 가능하다. 40~60개의 질문을 만들고 네 가지로 분류한다: 카테고리 수요(누가 어떤 일을 할 수 있는가), 선정 비교(A와 B 중 어느 쪽이 더 적합한가), 시나리오 페인 포인트(특정 구체적 문제를 어떻게 해결하는가), 브랜드 직접 조회(이 회사는 무엇을 하는 곳이며 신뢰할 수 있는가).

주류 모델에서 독립된 새 세션으로 항목별로 실행하고, 동일 질문을 최소 두 번 이상 반복하며 답변 원문을 기록한다.

지표 정의
언급률 브랜드명이 등장한 질문 수 / 전체 질문 수
최초 언급률 브랜드가 첫 번째 위치에 오른 질문 수 / 전체 질문 수
최우선 추천률 명확히 추천되었거나 더 적합하다고 표현된 질문 수 / 전체 질문 수
공식 웹사이트 인용률 공식 웹사이트를 인용하거나 링크한 질문 수 / 전체 질문 수
사실 정확도 브랜드 관련 사실을 정확히 서술한 질문 수 / 언급된 질문 수

그중 공식 웹사이트 인용률은 공식 웹사이트 개편의 가장 직접적인 지표이며, 나머지 항목은 사이트 외부 콘텐츠의 영향도 받습니다.

반드시 3~5개 모델을 커버해야 합니다. 근거는 2026년 9월 ChatGPT, Copilot, Google, Perplexity를 대상으로 한 관측 연구입니다:피인용 URL의 96.4%가 단일 엔진에서만 나타남, 교차 엔진 URL 중복도 중앙값은 0 (논문 Scoring With the Engine, arXiv:2609.22655). 모델 하나만 테스트하고 결론을 내리는 것은 테스트하지 않은 것과 같습니다.

기록표에서 가장 가치 있는 열은 「인용된 원문 문장」입니다. 이는 공식 웹사이트의 어떤 문장이 실제로 선택되었는지 알려줍니다. 이 문장을 더 앞쪽의 더 구조화된 블록으로 옮기는 것이 가장 효과적인 단일 지점 최적화입니다.

주기: 개편 전에 베이스라인을 남기고, 완료 후 1개월과 3개월에 각각 재측정하며, 이후 매월 고정된 하루에 실행합니다.

4. 여섯 가지 흔한 오판

1. 구조화 데이터를 추가하면 완료라고 생각한다. 마크업은 기계 인식을 해결하지만 콘텐츠 유무를 해결하지 못합니다. 추출할 문장이 없으면 아무리 완벽하게 마크업해도 인용되지 않습니다.

2. 홈페이지만 최적화한다. 인용되는 것은 구체적인 페이지입니다. 제품 페이지, Q&A 페이지, 사례 페이지가 주체이며, 홈페이지는 엔티티 정체성을 명확히 설명하는 역할만 합니다.

셋째, 「업계 선도」를 차별화 요소로 착각한다. 이런 단어는 인용할 수도 없고 검증할 수도 없으니, 없는 말이나 다름없다.

넷째, AI 크롤러를 허용하면 색인될 거라고 생각한다. 그건 입장권일 뿐이고, 인용될 수 있는지는 콘텐츠 품질이 결정한다.

다섯째, robots를 보안 수단으로 여긴다. 그건 권한 시스템이 아니다. 정말로 비밀로 해야 한다면 공개 웹페이지에 두지 마라.

여섯째, 한 번 고치면 끝이라고 생각한다. 모델은 변하고, 경쟁사도 바뀌고, 당신의 정보도 낡는다. 이건 유지보수 작업이지 프로젝트 산출물이 아니다.

자주 묻는 질문

Q: 공식 웹사이트 개편이 GEO에 얼마나 도움이 되나요?

A: 공식 웹사이트는 AI 교차 검증의 기준 정보원이다. 개편 전에는 AI가 본문을 읽지 못하고 엔티티를 인식하지 못하는 페이지를 받아들여 인용 확률이 매우 낮다. 개편 후에는 최소한 회수와 발췌의 조건을 갖추게 된다. 다만 유의할 점은, 공식 웹사이트 개편은 「인용할 근거가 있음」만 해결하며, 실제 인용 여부는 사이트 외부 콘텐츠 생태계에 달려 있고, 어떤 방법도 구체적인 향상 폭을 약속할 수 없다.

Q: 공식 웹사이트만 개편하면 충분한가요?

A: 부족하다. 공식 웹사이트는 주 진지이지만, AI는 여러 정보원을 종합해 판단한다. 공식 웹사이트를 잘 고치는 것은 AI가 인용할 근거를 갖추게 하는 것이고, 백과, 知乎, 업계 미디어의 콘텐츠는 AI가 인용할 의향이 있는지를 결정한다. 양쪽의 서술이 반드시 일치해야 하며, 그렇지 않으면 교차 검증에서 「정보 신뢰 불가」라는 결론이 나온다.

Q: 개편 후 얼마나 지나야 효과를 볼 수 있나요?

A: 색인 측면에서는 보통 2~4주 내에 크롤링 변화를 볼 수 있다. 답변 측면의 인용은 일반적으로 1~3개월이 걸리며, 여러 모델을 종합해 관찰해야 한다. 모델 답변은 변동이 크므로 단일 질문의 차이를 결론으로 삼지 말고, 연속 두 주기의 추세를 보라.

Q: 템플릿으로 만든 사이트도 GEO 최적화가 가능한가?

A: 고칠 수 있으면 할 수 있다. 진짜 걸림돌은 두 가지다. 렌더링 방식을 서버 측 출력으로 바꿀 수 있는지, 백엔드에서 페이지 head를 커스터마이즈할 수 있는지. 둘 다 안 되면 여섯 번째 레이어의 파이프라인은 사실상 무용지물이 되므로, 사이트 구축 방식을 바꾸는 것을 검토할 것을 권한다. 나머지 다섯 레이어는 콘텐츠 문제이며 사이트 구축 방식과 무관하다.


이 글은 《중국어 비유료 마케팅 100문》 시리즈의 일부입니다.전체 목차 보기 →

부록: GEO 공식 웹사이트 점검 체크리스트 (전체 복사 가능)

아래 내용은 전체를 복사하여 저장할 수 있으며, geo-site-checklist.md 독립적으로 사용하거나 자가 점검표로 인쇄할 수도 있다. 본문 중 {{}} 는 자리 표시자이며, 사용 시 교체한다.

0. 사용 방법

  1. 먼저 1절의 자가 점검 명령을 실행하여 5분 만에 기술 기반에 문제가 있는지 판단합니다.
  2. 2절의 6계층 점검 항목과 대조하여 하나씩 체크하고, 공백을 기록합니다.
  3. 7절의 우선순위에 따라 일정을 잡고, P0가 0이 되기 전에는 P1을 건드리지 않습니다.
  4. 템플릿은 바로 사용하세요: 구조화 데이터는 3절, robots는 4절, llms.txt는 5절, 문안 문장은 6절을 참고하세요.
  5. 출시 후 8절에 따라 모니터링을 구축합니다.

1. 자가 점검 명령

본문이 HTML에 있는지

curl -s https://{{DOMAIN}}/ | grep -E '<div id="(root|app|__next)"></div>'

출력이 있으면 순수 클라이언트 렌더링입니다. 출력이 없으면 본문 길이를 측정하세요:

curl -s https://{{DOMAIN}}/ | sed 's/<[^>]*>//g' | tr -d ' \n' | wc -m

500 미만이면 본문이 HTML에 들어가지 않았다고 판단할 수 있습니다.

H1과 구조화 데이터

curl -s https://{{DOMAIN}}/ | grep -c '<h1'
curl -s https://{{DOMAIN}}/ | grep -o '"@type" *: *"[^"]*"' | sort -u

첫 번째 항목은 1이어야 합니다. 두 번째가 비어 있으면 페이지에 구조화 데이터가 전혀 없다는 뜻입니다.

AI 크롤러가 잘못 차단되었는지

curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot" https://{{DOMAIN}}/
curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0 (Macintosh) AppleWebKit/537.36 Chrome/124.0 Safari/537.36" https://{{DOMAIN}}/

두 상태 코드가 일치하지 않으면 WAF 또는 안티 크롤링 규칙이 AI 크롤러를 차단하고 있다는 뜻입니다. 이는 robots.txt 문제보다 더 흔합니다.

두 고정 주소

https://{{DOMAIN}}/robots.txt
https://{{DOMAIN}}/llms.txt

각각 확인하세요: robots가 sitemap을 선언했는지, AI 크롤러 규칙을 명시적으로 작성했는지; llms.txt가 존재하는지.

2. 6계층 점검 항목

【1계층 엔티티 정체성】
[ ] 회사 전체 명칭
[ ] 브랜드 약칭
[ ] 영문명(있으면 기재)
[ ] 이전 명칭(있으면 기재)
[ ] 설립 시기
[ ] 등록지
[ ] 통합사회신용코드
[ ] 주요 사업 한 문장 정의(40자 이내)
[ ] 브랜드명과 제품명의 소속 관계, 제품 페이지에 제조·제공자 표기
[ ] 명칭, 주소, 전화번호, 사이트 전체 단일 버전
[ ] 외부 권위 프로필 링크(공상, 백과, 公众号, GitHub, 링크드인)
[ ] 위 항목 모두 순수 텍스트, 이미지 아님

【2계층 역량 경계】
[ ] 정의 문장: 무엇인지, 한 문장
[ ] 적용 대상
[ ] 적용 불가 상황(대안 제안 포함)
[ ] 산출물 목록
[ ] 납품 주기와 선행 조건
[ ] 가격 책정 방식 또는 가격 책정 논리
[ ] 서비스 지역, 언어, 진입 조건
[ ] 제한 사항과 선행 조건

【3계층 권위 증거】
[ ] 저자 서명 + 저자 페이지(이력, 자격, 외부 신원 링크)
[ ] 자격증 번호와 발급 기관
[ ] 특허 번호
[ ] 검사 보고서 번호와 검사 기관
[ ] 고객 사례: 업종, 배경, 공개 가능 데이터
[ ] 모든 숫자에 출처, 기준, 시점 표기
[ ] 페이지에 최종 업데이트 시각 표시
[ ] 핵심 페이지 수정 기록 보존
[ ] 편집 및 사실 검증 정책
[ ] 면책 조항

【4계층 차별화 비교】
[ ] 비교표(비교 방안 유형, 경쟁사 지목 없음)
[ ] 역량 매트릭스(기능 / 시나리오 / 규모 등급)
[ ] 「우리가 하지 않는 것」을 명확히 기재
[ ] 비교 시 비용과 트레이드오프 명시

【5계층 전환 경로】
[ ] 한 페이지에 주 CTA 하나만
[ ] 연락처 전 채널 일관성
[ ] 연락처에 복사 가능한 순수 텍스트 제공
[ ] 응답 약속: 얼마 만에, 누가 응답
[ ] 애프터서비스 및 보증 조항

【6계층 기계 판독 가능】
[ ] 서버 사이드 렌더링 또는 정적 생성
[ ] 페이지당 H1 하나
[ ] H2에 실제 의문문 사용
[ ] 본문을 main / article 내에 배치
[ ] Organization 구조화 데이터
[ ] Article 구조화 데이터
[ ] BreadcrumbList 구조화 데이터
[ ] Product / Service(페이지 유형별)
[ ] FAQPage(가시적 Q&A만 표기)
[ ] robots.txt에 AI 크롤러 명시적 선언
[ ] llms.txt 게시
[ ] sitemap 분할 및 lastmod 실제값
[ ] canonical 통일
[ ] hreflang 자체 일관 폐쇄(다국어 사이트)
[ ] 소프트 404 없음
[ ] 이미지 alt 완비
[ ] 핵심 데이터 텍스트 버전 제공

3. 구조화 데이터 템플릿

조직 엔터티(전체 사이트 모든 페이지에 삽입)

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "@id": "https://{{DOMAIN}}/#organization",
  "name": "{{브랜드 전체 명칭}}",
  "alternateName": ["{{브랜드 약칭}}", "{{영문명}}"],
  "legalName": "{{법인 등기 전체 명칭}}",
  "url": "https://{{DOMAIN}}/",
  "logo": {
    "@type": "ImageObject",
    "url": "https://{{DOMAIN}}/logo.png",
    "width": 512,
    "height": 512
  },
  "foundingDate": "{{2021-03-15}}",
  "description": "{{한 문장 정의, 회사 소개 첫 단락과 완전히 동일}}",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "{{상세 주소}}",
    "addressLocality": "{{도시}}",
    "addressRegion": "{{성/도}}",
    "postalCode": "{{우편번호}}",
    "addressCountry": "CN"
  },
  "contactPoint": [{
    "@type": "ContactPoint",
    "telephone": "{{+86-xxx}}",
    "contactType": "sales",
    "areaServed": "CN",
    "availableLanguage": ["zh-CN", "en"]
  }],
  "sameAs": [
    "{{법인 등기 정보 링크}}",
    "{{백과사전 링크}}",
    "{{公众号 또는 소셜 미디어 링크}}",
    "{{GitHub / 링크드인 / 채용 페이지}}"
  ],
  "knowsAbout": ["{{핵심 역량1}}", "{{핵심 역량2}}", "{{핵심 역량3}}"]
}

글 페이지

{
  "@context": "https://schema.org",
  "@type": "Article",
  "@id": "https://{{DOMAIN}}/{{PATH}}#article",
  "headline": "{{제목, 110자 이내}}",
  "description": "{{80-140자 요약, 브랜드명과 한 문장의 역량 정의 포함}}",
  "image": ["https://{{DOMAIN}}/{{커버 이미지}}"],
  "datePublished": "{{2026-10-23T09:00:00+08:00}}",
  "dateModified": "{{2026-10-23T09:00:00+08:00}}",
  "inLanguage": "zh-CN",
  "author": {
    "@type": "Person",
    "@id": "https://{{DOMAIN}}/author/{{slug}}#person",
    "name": "{{작성자명}}",
    "jobTitle": "{{직책}}",
    "url": "https://{{DOMAIN}}/author/{{slug}}"
  },
  "publisher": { "@id": "https://{{DOMAIN}}/#organization" },
  "isPartOf": { "@id": "https://{{DOMAIN}}/#website" },
  "about": [{ "@type": "Thing", "name": "{{주제어}}" }],
  "citation": ["{{인용한 외부 출처 URL}}"]
}

publisher.@id 반드시 조직 엔터티의 @id 와 완전히 동일해야 하며, 그렇지 않으면 두 노드가 그래프에서 끊어진다.

Q&A 블록

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [{
    "@type": "Question",
    "name": "{{사용자가 실제로 물을 질문}}",
    "acceptedAnswer": {
      "@type": "Answer",
      "text": "{{40-120자, 결론 우선, 서론 없이}}"
    }
  }]
}

4. robots.txt 템플릿

# 검색 유형: 반드시 허용
User-agent: OAI-SearchBot
Allow: /

User-agent: ChatGPT-User
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Claude-SearchBot
Allow: /

User-agent: Baiduspider
Allow: /

User-agent: YisouSpider
Allow: /

# 학습 유형: 브랜드 전략에 따라 결정
User-agent: GPTBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: Bytespider
Allow: /

# 민감한 경로는 전체 차단
User-agent: *
Disallow: /admin/
Disallow: /api/
Allow: /

Sitemap: https://{{DOMAIN}}/sitemap.xml

CSS와 JS 리소스를 Disallow하지 마세요. 일부 엔진은 페이지 구조를 판단하는 데 이를 필요로 합니다.

5. llms.txt 템플릿

# {{브랜드명}}

> {{한 문장 정의: A 업종의 B 고객에게 C 서비스를 제공하여 D 문제를 해결합니다. 설립 시기와 소재지도 함께 명시합니다.}}

## 핵심 페이지
- [{{제품명}}](https://{{DOMAIN}}/{{path}}): {{해당 페이지가 답하는 질문}}
- [{{솔루션}}](https://{{DOMAIN}}/{{path}}): {{한 문장 설명}}
- [가격](https://{{DOMAIN}}/pricing): {{과금 방식 한 문장}}

## 권위 정보
- [회사 소개](https://{{DOMAIN}}/about): 회사 정식 명칭, 설립 시기, 자격, 팀
- [고객 사례](https://{{DOMAIN}}/cases): {{업종과 공개 가능한 결과}}
- [자주 묻는 질문](https://{{DOMAIN}}/faq): {{다루는 주제}}

## 콘텐츠 섹션
- [{{섹션명}}](https://{{DOMAIN}}/{{path}}/): {{섹션 포지셔닝}}

## 경계와 고지
- 서비스 지역: {{...}}
- 적용 불가 시나리오: {{...}}
- 콘텐츠 업데이트: {{빈도}}

## Optional
- [{{하위 페이지}}](https://{{DOMAIN}}/{{path}}): 보충 자료

첫 단락 blockquote는 통째로 인용되므로 독립적으로 성립해야 합니다. 각 링크 뒤의 설명 문장은 생략할 수 없습니다.

6. 답변 단위 문장

정의 문장
{{브랜드/제품}}은(는) {{카테고리}}이며, {{대상}}에게 {{역량}}을 제공하여 {{문제}}를 해결합니다.

경계 문장
{{조건A}}, {{조건B}}의 시나리오에 적용됩니다. 만약 {{반조건}}이라면 {{대안}}이 더 적합합니다.

비교 문장
{{방안 유형A}}와 비교할 때, {{본 제품}}은(는) {{차원}}에서 {{차이}}가 있으며, 그 대가는 {{트레이드오프}}입니다.

데이터 문장
{{지표}}는 {{수치}}입니다(출처: {{기관 또는 내부 통계}}, 기준: {{정의}}, 시점: {{연월}}).

자체 점검 기준: 형용사를 모두 삭제한 뒤에도 문장이 성립하는지 확인하세요. 삭제 후 '우리는 양질의 서비스 제공에 힘쓰고 있습니다'만 남는다면 그것은 마케팅 문구입니다.

문단 규범: 2~4문장, 결론은 첫 문장에; 핵심 정보는 표로; 시간은 절대값으로 표기하고 「최근」이라고 쓰지 않음; 숫자는 단위와 기준을 포함; 핵심 데이터는 이미지에만 존재해서는 안 됨.

7. 우선순위와 공정 기간

레벨 콘텐츠 기간
P0 SSR 또는 정적화; 엔터티 신원 단락; 페이지당 한 문장 정의; robots 및 AI 크롤러 전략; Organization 및 Article 마크업 2주
P1 Q&A 형식 소섹션 재구성; 저자 페이지, 자격 페이지, 사례 페이지; llms.txt; sitemap 및 hreflang 관리 1개월
P2 비교 매트릭스; 가격 정보; 모니터링 체계 지속

P0 내부 순서: 렌더링 → 엔터티 → 정의 문장 → 크롤러 전략 → 구조화 데이터. 앞 단계가 처리되지 않으면 뒤 단계는 무효.

8. 검수 기록

질문 세트 4가지 문형

카테고리 수요: 국내에서 {{카테고리}} 서비스를 제공하는 업체는 어디인가
선택 비교: {{방안A}}와 {{방안B}} 중 {{규모}}의 {{대상}}에 더 적합한 것은
시나리오 페인포인트: {{구체적 문제}}를 어떻게 해결하는가
브랜드 직접 조회: {{브랜드}}는 무엇을 하는 곳이며 신뢰할 수 있는가

다섯 가지 지표

지표 정의
언급률 브랜드명이 등장한 질문 수 / 전체 질문 수
최초 언급률 브랜드가 첫 번째 위치에 오른 질문 수 / 전체 질문 수
최우선 추천률 명확히 추천되었거나 더 적합하다고 표현된 질문 수 / 전체 질문 수
공식 웹사이트 인용률 공식 웹사이트를 인용하거나 링크한 질문 수 / 전체 질문 수
사실 정확도 브랜드 관련 사실을 정확히 서술한 질문 수 / 언급된 질문 수

기록표 필드

날짜 | 모델 | 질문ID | 질문 텍스트 | 웹 검색 여부 | 언급 여부 | 순위 | 추천 여부 | 감성 | 공식 사이트 인용 여부 | 인용 페이지 URL | 인용된 원문 문장 | 사실 오류

「인용된 원문 문장」은 가장 가치 있는 열입니다: 어떤 문장이 실제로 선택되었는지 알려주며, 이 문장을 더 앞쪽의 더 구조화된 블록으로 옮기는 것이 가장 효과적인 단일 지점 최적화입니다.

주기: 개조 전 베이스라인을 남기고, 완료 후 1개월과 3개월에 각각 재측정하며, 이후 매월 고정된 하루에 실행. 최소 3~5개 모델을 커버.

부록: 전체 SKILL 문서 (원클릭 복사 가능)

위 부록은 사람이 대조하기 위한 체크리스트이며, 여기는 AI 도구에 바로 탑재할 수 있는 전체 SKILL입니다. 워크플로 정의, 6계층 점검 체크리스트, 모든 구조화 데이터 템플릿, AI 크롤러 전략, 답변 단위 문형, 검수 방법, 그리고 표준 라이브러리만 의존하는 진단 스크립트를 포함합니다.

복사 후 파일 헤더의 <!-- ===== 파일명 ===== --> 주석에 따라 해당 파일로 분리하면 됩니다. 디렉터리 구조는 다음과 같습니다:

geo-site-revamp/
├── SKILL.md
├── scripts/audit_site.py
└── references/
    ├── audit-checklist.md
    ├── schema-templates.md
    ├── ai-crawler-policy.md
    ├── answer-unit-patterns.md
    └── monitoring.md
Markdown 형식, 약 30,489자
<!-- ===== SKILL.md ===== -->

---
name: geo-site-revamp
description: 审计并改造企业官网以满足生成式引擎优化(GEO)要求,让品牌在 AI 搜索与对话式回答中被提及、被引用。当用户要做 GEO 官网改造、AI 搜索优化、官网结构化数据、llms.txt、AI 爬虫策略、AEO、品牌在 AI 回答中的可见度,或需要对站点做 GEO 体检时使用。包含可直接执行的站点体检脚本、六层信息点清单、JSON-LD 模板、答案单元句式与验收指标。
---

# GEO 官网改造

## 核心判断

被大模型引用的最小单位不是页面,而是一个能被单独摘走的句子或字段。改造的一切动作都服务于:让官网存在足够多、足够准、足够结构化的「答案单元」。

两类信息点必须同时补:

- **事实型**:可结构化、可交叉校验的客观字段(实体信息、参数、价格、资质)。
- **叙述型**:可整段摘录的定义句、边界句、对比句、数据句。

只补 schema 不改正文,不会被引用;只改正文不加结构化数据,召回效率低。

## 工作流

### 1. 确定范围

确认目标域名、主要语言、业务类型(B2B 服务 / 电商 / 内容站 / 多语言站),以及是否已有 sitemap、robots、llms.txt。业务类型决定第 2、4 层的具体写法。

### 2. 跑体检

```bash
python3 scripts/audit_site.py https://example.com --max-pages 5 --json geo-audit.json
```

脚本检查:robots 与各 AI 爬虫策略、llms.txt、sitemap、每页 H1/H2 数量、JSON-LD 类型、canonical、hreflang、alt 缺失、正文长度、纯 CSR 迹象,并用 GPTBot UA 复抓首页对比是否被反爬误拦。输出按 P0/P1/P2 排序的改造项。

脚本只覆盖第 6 层(机器可读载体)和第 1 层(实体 schema)。第 2-5 层是内容问题,必须人工读页面判断。

### 3. 逐层诊断

按 `references/audit-checklist.md` 的六层顺序过一遍,产出缺口表:

1. 实体身份:名称、成立时间、注册地、一句话定义、NAP、sameAs。
2. 能力边界:定义句、适用与不适用、交付物、计价、限制条件。
3. 权威证据:作者页、资质编号、案例数据、更新时间、事实核查政策。
4. 差异化对比:对比表、能力矩阵、「我们不做什么」。
5. 转化路径:唯一 CTA、可复制联系方式、响应承诺。
6. 机器可读:SSR、语义化 HTML、schema、robots、llms.txt、sitemap、canonical/hreflang。

### 4. 改造与产出

按 P0 → P1 → P2 推进。写代码时遵循:

- schema 按 `references/schema-templates.md` 注入,全站用同一个 `@id` 串成图;只标记页面真实可见的内容。
- 页面文案按 `references/answer-unit-patterns.md` 重写,一页八问必须答到五项以上。
- robots 与 llms.txt 按 `references/ai-crawler-policy.md` 落地,配置后用真实 UA 实测状态码与正文长度。
- 纯 CSR 站点必须先改服务端渲染或静态生成,其余优化在此之前没有意义。

### 5. 验收

按 `references/monitoring.md` 建品牌问题集并留基线。验收看四项:体检 P0 归零、官网引用率相对基线提升、事实准确率 ≥90%、核心品类问题能被官网信息覆盖。

## 资源索引

| 文件 | 用途 |
|---|---|
| `scripts/audit_site.py` | 站点 GEO 体检,输出优先级化改造项 |
| `references/audit-checklist.md` | 六层信息点清单、优先级、常见误判 |
| `references/schema-templates.md` | Organization/WebSite/Article/Product/FAQPage/Breadcrumb JSON-LD 模板 |
| `references/ai-crawler-policy.md` | AI 爬虫清单、robots 模板、llms.txt 模板 |
| `references/answer-unit-patterns.md` | 五类可引用的句式模板与段落规范 |
| `references/monitoring.md` | 问题集设计、指标定义、记录表、归因纪律 |

## 输出规范

- 诊断结果按「层级 → 缺口 → 改造动作 → 优先级」四列呈现,不要只给问题清单。
- 涉及多语言站时,检查各语言的实体信息是否一致,hreflang 是否闭合。
- 不要建议伪造评分、虚构客户、编造数据,AI 交叉验证会反噬品牌。
- 不要承诺「做了就能被引用」,GEO 只能提高概率,答案由模型侧决定。

<!-- ===== references/audit-checklist.md ===== -->

# 官网 GEO 改造六层信息点清单

改造目标不是「让爬虫收录页面」,而是「让大模型在回答某类问题时,愿意用你的官网内容当依据,并在答案里点到你的名字」。

被引用的最小单位不是页面,是**一个可以被直接摘走的句子或字段**。所以清单按两类信息点组织:

- **事实型**:可结构化、可交叉校验的客观字段(名称、地址、参数、价格、资质)。
- **叙述型**:可被整段摘录的结论句、定义句、对比句(是什么、适合谁、和谁不同)。

大多数官网的现状是事实散落、叙述空洞,两类都缺。

---

## 第 1 层 实体身份:AI 要能确认「你是哪家公司」

优先级最高且最常被忽略。大模型把品牌当作知识图谱中的一个实体,找不到唯一指代就会混同或干脆不提。

| 信息点 | 常见问题 | 改造要求 |
|---|---|---|
| 公司全称 / 简称 / 英文名 / 曾用名 | 全站只有品牌简称 | 「关于我们」首段一句话讲清四者对应关系 |
| 成立时间 / 注册地 / 统一社会信用代码 | 完全没有 | 纯文本列出,不要做成图片 |
| 主营一句话定义 | 「致力于打造生态闭环」 | 换成「我们为 A 行业的 B 客户提供 C 服务,解决 D 问题」 |
| 品牌名与产品名的隶属关系 | 产品页不提出品方 | 每个产品页标注「由 X 出品」 |
| NAP(名称/地址/电话) | 页脚、联系页、招聘页三处不一致 | 收敛为全站唯一版本 |
| sameAs 外部权威档案 | 无 | 工商、百科、公众号、GitHub、领英、招聘页互指 |

硬要求:外部档案上的说法必须与官网**逐字一致**。AI 交叉验证时不一致会降低实体置信度。

## 第 2 层 能力边界:AI 要能判断「你是不是这个问题的答案」

用户问 AI 时最常见的形式是「谁能做某件事」。每项业务都要有一份「标准答案段」,而不是营销文案。

- **定义句**:是什么,一句话,不超过 40 字。
- **适用对象**:明确「适合谁」和「不适合谁」。有边界的表述才可能被引用,因为 AI 要回答「是否适合我」。
- **交付物与周期**:细到可以列清单。
- **计价方式**:区间或计价逻辑。可以不公开价格,但不能让 AI 无话可说。
- **服务区域 / 语言 / 准入条件**。
- **限制与前置条件**:反直觉但关键,它是可信度的来源。

## 第 3 层 权威证据:AI 要能回答「凭什么信你」

- 作者或专家署名 + 个人页(履历、资质、可验证的外部身份)。
- 资质证书、专利、检测报告:写编号与发证机构,不要只放扫描图。
- 客户案例:行业、项目背景、可公开的结果数据。
- 数据出处:数字标注来源、统计口径、时间。
- 页面显示「最后更新时间」,关键页有修订记录。
- 编辑与事实核查政策、免责声明(国内站点几乎都没做,做了就是差异化)。

## 第 4 层 差异化对比:AI 要能回答「和别家比怎么样」

AI 回答选型类问题必然做比较。官网没有对比信息,AI 就去第三方来源拼凑,而第三方评价你控制不了。

- 与同类方案的对比表(对比「品类/方案类型」,不点名对手)。
- 能力矩阵:按功能、场景、规模分档。
- 明确写下「我们不做什么」「什么场景下我们不是最优解」。

## 第 5 层 转化与信任路径:AI 的答案要能落到一个动作

- 唯一首选行动入口:咨询 / 试用 / 报价 / 预约,一页只给一个主 CTA。
- 联系方式全渠道一致,提供可复制的纯文本(不要只有图标或二维码)。
- 服务响应承诺:多久回、谁回。
- 售后与保障条款。

## 第 6 层 机器可读载体:把前五层送出去

按优先级:

1. **服务端渲染或静态生成**。AI 爬虫普遍不执行 JS,纯 CSR 的正文等于不存在。P0。
2. **语义化 HTML**:每页一个 H1,H2 用真实问题句式,正文在 `main`/`article` 内。
3. **结构化数据**:`Organization`(含 `@id`、`sameAs`)、`WebSite`、`Article`(`author`/`datePublished`/`dateModified`)、`Product`/`Offer`、`FAQPage`、`HowTo`、`BreadcrumbList`。用 `@id` 把作者、发布方、主站实体串成一张图,避免孤岛。
4. **robots.txt 区分 AI 爬虫用途**(检索 vs 训练),见 `ai-crawler-policy.md`。
5. **llms.txt / ai.txt**:站点摘要 + 关键页面索引。
6. **sitemap.xml**:分语言、分内容类型,`lastmod` 真实。
7. **canonical 与 hreflang 治理**:多语言站必须自洽闭合,否则各语言版本互相镜像。
8. **404 与重定向治理**:软 404 会拖低站点质量判定。
9. **alt 与表格文本化**:关键数据不能只存在于图片里,AI 读不到图里的数字。

---

## 优先级与工期

| 级别 | 内容 | 工期 |
|---|---|---|
| P0 | SSR/静态化、实体身份段、每页一句话定义、robots 与 AI 爬虫策略、Organization 与 Article schema | 2 周 |
| P1 | 问答式小节重构、权威证据页(作者/资质/案例)、llms.txt、sitemap 与 hreflang 治理 | 1 个月 |
| P2 | 对比矩阵、计价信息、监测机制 | 持续 |

## 验收

- 体检脚本 P0 归零,P1 逐项消解:`scripts/audit_site.py <站点域名>`
- 品牌问题集的官网覆盖率(见 `monitoring.md`)
- 各模型下的提及率、首位提及率、首选推荐率、正负面提及率的变化

## 常见误判

- 加了一堆 schema 但正文没有可引用的句子 → 仍不会被引用。schema 是标签,不是内容。
- 只优化首页 → AI 引用的是具体页面,产品页、问答页、案例页才是主战场。
- 把「行业领先」「赋能」当差异化 → 这类词无法被摘取,等于没说。
- 给 AI 爬虫开白就等于被收录 → 只是入场券,能否被引用由内容质量决定。

<!-- ===== references/schema-templates.md ===== -->

# JSON-LD 模板(GEO 用)

原则:**全站一张图**,不是一堆碎片。用 `@id` 让文章、作者、发布方、主站互相指向同一个实体节点。
把占位符(`{{...}}`)替换后直接内联到 `<head>` 或 `<body>` 末尾。

## 1. 组织实体(全站每页注入,P0)

```json
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "@id": "https://{{DOMAIN}}/#organization",
  "name": "{{公司全称}}",
  "alternateName": ["{{品牌简称}}", "{{英文名}}"],
  "legalName": "{{工商登记全称}}",
  "url": "https://{{DOMAIN}}/",
  "logo": {
    "@type": "ImageObject",
    "url": "https://{{DOMAIN}}/logo.png",
    "width": 512,
    "height": 512
  },
  "foundingDate": "{{YYYY-MM-DD}}",
  "description": "{{一句话定义:为 A 行业的 B 客户提供 C 服务,解决 D 问题}}",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "{{详细地址}}",
    "addressLocality": "{{城市}}",
    "addressRegion": "{{省}}",
    "postalCode": "{{邮编}}",
    "addressCountry": "CN"
  },
  "contactPoint": [{
    "@type": "ContactPoint",
    "telephone": "{{+86-xxx}}",
    "contactType": "sales",
    "areaServed": "CN",
    "availableLanguage": ["zh-CN", "en"]
  }],
  "sameAs": [
    "{{工商档案链接}}",
    "{{百科链接}}",
    "{{公众号或社媒链接}}",
    "{{GitHub / 领英 / 招聘主页}}"
  ],
  "knowsAbout": ["{{核心能力1}}", "{{核心能力2}}", "{{核心能力3}}"]
}
```

要点:
- `legalName` 与 `name` 分开,工商全称和对外品牌名不同时尤其重要。
- `sameAs` 至少 3 条,且每个链接页面上必须能找到公司全称。
- `knowsAbout` 填能力词,这是引擎判断「你属于哪个领域」的直接信号。

## 2. 站点节点

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "@id": "https://{{DOMAIN}}/#website",
  "url": "https://{{DOMAIN}}/",
  "name": "{{站点名}}",
  "inLanguage": "zh-CN",
  "publisher": { "@id": "https://{{DOMAIN}}/#organization" }
}
```

## 3. 文章页

```json
{
  "@context": "https://schema.org",
  "@type": "Article",
  "@id": "https://{{DOMAIN}}/{{PATH}}#article",
  "headline": "{{标题,<=110 字符}}",
  "description": "{{80-140 字摘要,含实体名与一句能力定义}}",
  "image": ["https://{{DOMAIN}}/{{封面图}}"],
  "datePublished": "{{2026-01-01T09:00:00+08:00}}",
  "dateModified": "{{2026-01-01T09:00:00+08:00}}",
  "inLanguage": "zh-CN",
  "author": {
    "@type": "Person",
    "@id": "https://{{DOMAIN}}/author/{{slug}}#person",
    "name": "{{作者名}}",
    "jobTitle": "{{职位}}",
    "url": "https://{{DOMAIN}}/author/{{slug}}",
    "sameAs": ["{{作者可验证的外部身份链接}}"]
  },
  "publisher": { "@id": "https://{{DOMAIN}}/#organization" },
  "isPartOf": { "@id": "https://{{DOMAIN}}/#website" },
  "about": [{ "@type": "Thing", "name": "{{主题词}}" }],
  "citation": ["{{引用的外部权威来源 URL}}"]
}
```

`dateModified` 必须真实更新,否则是自毁可信度。`citation` 是 GEO 里价值最高的字段之一。

## 4. 产品 / 服务

```json
{
  "@context": "https://schema.org",
  "@type": "Product",
  "@id": "https://{{DOMAIN}}/{{PATH}}#product",
  "name": "{{产品名}}",
  "description": "{{一句话定义 + 适用对象}}",
  "brand": { "@id": "https://{{DOMAIN}}/#organization" },
  "category": "{{品类}}",
  "offers": {
    "@type": "Offer",
    "priceCurrency": "CNY",
    "price": "{{价格或起价}}",
    "priceSpecification": {
      "@type": "PriceSpecification",
      "priceCurrency": "CNY",
      "minPrice": "{{起}}",
      "maxPrice": "{{止}}"
    },
    "availability": "https://schema.org/InStock",
    "url": "https://{{DOMAIN}}/{{PATH}}"
  }
}
```

纯服务型业务用 `Service` + `areaServed` + `provider`,不要硬套 `Product`。

## 5. 问答页 / FAQ 区块

```json
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [{
    "@type": "Question",
    "name": "{{用户真实会问的问题}}",
    "acceptedAnswer": {
      "@type": "Answer",
      "text": "{{40-120 字直接回答,结论先行,不铺垫}}"
    }
  }]
}
```

只标记页面上**真实可见**的问答。隐藏内容标记 FAQ 会被判作弊。

## 6. 面包屑

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "首页", "item": "https://{{DOMAIN}}/" },
    { "@type": "ListItem", "position": 2, "name": "{{栏目}}", "item": "https://{{DOMAIN}}/{{栏目路径}}/" }
  ]
}
```

## 7. 多语言站

每个语言版本各自注入本地化的 Organization 与 Article,`inLanguage` 与站点 `hreflang` 严格对应;`@id` 用同一路径 + 语言后缀区分,不要复用同一个 `@id`。

## 校验

- Google Rich Results Test、Schema Markup Validator 过一遍语法。
- 用同一 `@id` 检查是否真的连成图:文章 `publisher.@id` 是否等于组织节点 `@id`。
- 禁止把不存在的信息写进 schema(虚构评分、虚构价格),AI 交叉验证会反噬。

<!-- ===== references/ai-crawler-policy.md ===== -->

# AI 爬虫策略与 llms.txt 模板

## 先分清三种用途

AI 侧访问官网有三种动机,对应完全不同的决策:

| 用途 | 行为 | 你想要的 |
|---|---|---|
| 训练语料抓取 | 批量、低频、抓全站 | 视策略而定,多数 B 端品牌可以放开,换取模型内部知识 |
| 搜索索引构建 | 定期抓取,用于生成答案时召回 | **必须允许**,这是被引用的前提 |
| 用户触发的实时浏览 | 用户提问时当场打开你的页面 | **必须允许**,且页面要能快速渲染出正文 |

只关心「被引用」的话,第二、三类是关键;只禁第一类通常没有意义,因为同一批内容往往也服务检索。

## 机器人清单

以各厂商官方文档为准,UA 会变,配置前复核。

| UA 标识 | 归属 | 主要用途 |
|---|---|---|
| `GPTBot` | OpenAI | 训练与内容抓取 |
| `OAI-SearchBot` | OpenAI | 搜索索引,用于 ChatGPT 搜索答案 |
| `ChatGPT-User` | OpenAI | 用户触发实时访问 |
| `ClaudeBot` / `anthropic-ai` | Anthropic | 训练抓取 |
| `Claude-User` / `Claude-SearchBot` | Anthropic | 用户触发 / 检索 |
| `PerplexityBot` | Perplexity | 检索与引用 |
| `Google-Extended` | Google | 控制 Gemini 训练,不影响搜索收录 |
| `Bytespider` | 字节跳动 | 豆包 / 头条生态 |
| `Baiduspider` | 百度 | 百度搜索,兼供百度 AI 生态 |
| `YisouSpider` | 神马 / 夸克生态 | 检索 |
| `Applebot-Extended` | Apple | Apple Intelligence 训练控制 |
| `CCBot` | Common Crawl | 公共语料,被大量模型间接使用 |
| `meta-externalagent` | Meta | 训练抓取 |

## robots.txt 模板

```
# 搜索与检索类:必须允许,否则等于放弃被引用
User-agent: OAI-SearchBot
Allow: /

User-agent: ChatGPT-User
Allow: /

User-agent: PerplexityBot
Allow: /

User-agent: Claude-SearchBot
Allow: /

User-agent: Baiduspider
Allow: /

User-agent: YisouSpider
Allow: /

# 训练类:按品牌策略决定,下面这组是「允许」
User-agent: GPTBot
Allow: /

User-agent: ClaudeBot
Allow: /

User-agent: Google-Extended
Allow: /

User-agent: Bytespider
Allow: /

User-agent: CCBot
Allow: /

# 敏感路径对全部爬虫关闭(后台、报价策略、客户资料)
User-agent: *
Disallow: /admin/
Disallow: /api/
Disallow: /uploads/private/
Allow: /

Sitemap: https://{{DOMAIN}}/sitemap.xml
```

注意事项:

- `Disallow: /` 只对匹配到的组生效,别指望一行挡住所有爬虫。
- 不要 `Disallow` 掉 CSS/JS 资源,部分引擎需要它判断页面结构。
- 反爬 WAF 对 AI UA 的误拦比 robots 更常见。配置后必须用真实 UA 实测:`curl -A "<UA>" -I https://{{DOMAIN}}/`,状态码和正文长度要与浏览器一致。
- 不要用 `Disallow` 当保密手段。真要保密就别放在公网页面里。

## llms.txt 模板

放在站点根,`https://{{DOMAIN}}/llms.txt`。目标是让模型用最少 token 理解你是谁、有哪些页面值得读。

```
# {{品牌名}}

> {{一句话定义:为 A 行业的 B 客户提供 C 服务,解决 D 问题。成立时间、所在地、规模等实体信息一句话内交代。}}

## 核心页面
- [{{产品/服务名}}](https://{{DOMAIN}}/{{path}}): {{一句话说明该页回答什么问题}}
- [{{解决方案名}}](https://{{DOMAIN}}/{{path}}): {{说明}}
- [定价](https://{{DOMAIN}}/pricing): {{计价方式一句话}}

## 权威信息
- [关于我们](https://{{DOMAIN}}/about): 公司全称、成立时间、资质、团队
- [客户案例](https://{{DOMAIN}}/cases): {{行业与可公开结果}}
- [常见问题](https://{{DOMAIN}}/faq): {{覆盖的问题主题}}

## 内容栏目
- [{{栏目名}}](https://{{DOMAIN}}/{{path}}/): {{栏目定位}}

## 边界与声明
- 服务区域:{{...}}
- 不适用场景:{{...}}
- 内容更新:{{更新频率}}

## Optional
- [{{次级页面}}](https://{{DOMAIN}}/{{path}}): 补充材料
```

写法要求:

- 首段 blockquote 就是「标准答案段」,模型可能整段引用,必须能独立成立。
- 每个链接后面必须有一句说明,否则只是一张 URL 表,价值极低。
- 全站信息变了要同步更新,过期摘要比没有更糟。
- 多语言站可为每种语言各出一份,用 `llms-<lang>.txt`,并在主文件里指路。

## ai.txt(可选)

若干社区约定在用,非官方标准。内容是允许/禁止 AI 使用的声明与联系邮箱。写了不亏,但不要指望它起决定作用。真正的控制手段是 robots 与访问控制。

<!-- ===== references/answer-unit-patterns.md ===== -->

# 可被引用的答案单元写法

模型不会整篇引用,它摘句子。所以每一段都要能单独成立:脱离上下文后,读者仍知道「谁、做什么、对谁、有什么条件」。

## 五类必备句式

### 1. 定义句(每页必有,放在首段第二句内)

模板:`{{品牌/产品}}是{{品类}},为{{对象}}提供{{能力}},用于解决{{问题}}。`

差:我们致力于以创新技术赋能企业数字化转型。
好:XX 是企业级数据备份服务,为 200 人以上的研发团队提供跨云增量备份,用于解决多环境数据一致性校验困难的问题。

判定标准:把句子里所有形容词删掉,信息量是否基本不变。变了说明靠形容词撑场面,删掉后句子仍然成立才合格。

### 2. 边界句(决定能否被回答「适不适合我」)

模板:`适用于{{条件 A}}、{{条件 B}}的场景;如果{{反条件}},更适合{{替代方案}}。`

写「不适合谁」不会丢客户,反而提升被引用概率,因为模型需要给出推荐理由和排除理由。全站没有排除句,等于把自己排除在选型类问答之外。

### 3. 对比句(用于选型问题)

模板:`与{{方案类型 A}}相比,{{本产品}}在{{维度}}上{{差异}},代价是{{取舍}}。`

对比「方案类型」而不是点名对手,既避免法律风险,又覆盖用户真实的比较语境。必须写出代价,只讲优势的对比会被判定为营销内容。

### 4. 数据句(提升可信度权重)

模板:`{{指标}}为{{数值}}(来源:{{机构/内部统计}},口径:{{定义}},时间:{{年月}})。`

没有来源的数字在 GEO 里是负资产。宁可给一个带口径的小数字,也不要给一个没出处的漂亮数字。

### 5. 问答句(H2/H3 直接写成问题)

用户向 AI 提问用的是问句,页面小节标题也用问句,召回匹配度最高。

- 好:`多环境备份如何保证一致性?`、`XX 和自建备份方案的差别在哪?`
- 差:`产品优势`、`技术架构`、`解决方案概述`

## 段落与版式规范

- 段落 2-4 句,一句一个信息点,不写长复合句。
- 结论先行,背景后置。第一句就是答案。
- 关键信息用表格承载:参数、对比、计价、资质。表格是最容易被完整摘取的格式。
- 列表项写成完整短句,不要写成「高效」「稳定」「安全」这类单词标签。
- 时间写绝对时间(2026 年 3 月),不写「近期」「目前」。
- 数字带单位和口径,百分比注明基数。
- 关键数据不要只放在图片或图表里,必须有一份文字版。

## 页面骨架检查

一页合格的产品/服务页,按顺序应该能回答:

1. 这是什么(定义句)
2. 解决谁的什么问题(场景)
3. 适合与不适合(边界句)
4. 具体交付什么、多久(清单)
5. 怎么计价(计价逻辑)
6. 谁在用、效果如何(案例 + 数据句)
7. 和别的方式比差在哪(对比句)
8. 怎么开始(唯一 CTA)

八项缺三项以上,这页基本不会被引用。

## 自检口诀

逐段问一句:**这段话能不能直接贴进别人的回答里,而不显得突兀?** 能,就是合格的答案单元;不能,就是给人看的营销文案。

<!-- ===== references/monitoring.md ===== -->

# 效果监测与验收

GEO 没有后台数据可看,只能靠「模拟用户提问 + 记录答案」。所以监测方案要先建起来,否则改造完无法判断有没有用。

## 1. 建品牌问题集

按四类各取 8-15 条,共 40-60 条。问题必须写成真实用户口吻,不要写成关键词。

| 类型 | 示例 | 考察点 |
|---|---|---|
| 品类需求 | 「国内做跨云数据备份的服务商有哪些」 | 是否被列入候选 |
| 选型对比 | 「A 方案和 B 方案哪个更适合 200 人研发团队」 | 是否被当作比较对象 |
| 场景痛点 | 「多环境数据库备份不一致怎么解决」 | 是否被当作解决方案 |
| 品牌直查 | 「XX 公司是做什么的,靠谱吗」 | 实体是否被正确识别 |

固定问题集,不要每次换题,否则无法纵向比较。

## 2. 探测方法

- 每个模型用**独立新会话**,避免上下文污染。
- 同一问题在同一模型至少跑 2 次,结果可能波动,取稳定表现。
- 记录答案原文,不要只记结论。
- 覆盖主流入口:DeepSeek、豆包、元宝、通义、Kimi、百度 AI 搜索,以及带联网检索的通用助手。
- 有联网能力的模式与纯模型模式分开记录,前者反映检索与引用能力,后者反映模型内部知识。

## 3. 指标定义

| 指标 | 定义 |
|---|---|
| 提及率 | 答案中出现品牌名的题数 / 总题数 |
| 首位提及率 | 品牌出现在第一个被推荐位置的题数 / 总题数 |
| 首选推荐率 | 被明确表述为「推荐」「更适合」「首选」的题数 / 总题数 |
| 官网引用率 | 答案引用或链接到官网的题数 / 总题数 |
| 正面提及率 / 负面提及率 | 含明确正/负面评价的题数 / 总题数 |
| 事实准确率 | 品牌相关事实(业务、规模、价格)表述正确的题数 / 被提及题数 |

「官网引用率」是官网改造最直接的效果指标。其他指标还受站外内容影响,改造周期内不要混为一谈。

## 4. 记录表字段

```
日期 | 模型 | 问题ID | 问题文本 | 是否强调联网 | 品牌是否提及 | 位次 | 是否推荐 | 情感 | 是否引用官网 | 引用页面URL | 引用到的原句 | 事实错误 | 截图/链接
```

「引用到的原句」这一列最重要:它直接告诉你官网上哪句话被模型选中了,把这句挪到更靠前的位置、更结构化的区块,是最有效的迭代动作。

## 5. 迭代节奏

- **改造前**:跑一遍全量问题集,留基线,同时跑体检脚本存档。
- **改造中**:每完成一层,抽 10 条核心问题复测。
- **改造后 1 个月、3 个月**:全量复测,对比基线。
- 每月固定一天跑,形成时间序列。

## 6. 归因纪律

- 模型答案波动大,单次差异不算结论,看连续两个周期的趋势。
- 官网改造主要影响「引用率」和「事实准确率」;提及率和首选推荐率还取决于第三方内容生态,不能全部归功或归咎于官网。
- 出现负面提及,先查信息源,再改官网对应页面的表述,不要直接删内容。

## 7. 验收标准

1. 体检脚本 P0 全部归零。
2. 问题集中「官网引用率」相对基线提升。
3. 「事实准确率」达到 90% 以上(说明实体信息已经统一)。
4. 核心品类问题中,官网信息能覆盖答案里的关键论点(人工比对)。

<!-- ===== scripts/audit_site.py ===== -->

保存为 scripts/audit_site.py,用法:

```bash
python3 scripts/audit_site.py https://example.com --max-pages 8 --json report.json
```

```python
#!/usr/bin/env python3
"""GEO 官网体检:抓取页面并输出可改造项清单。

只依赖标准库。检查项对应 references/audit-checklist.md 的前五层。

用法:
    python3 audit_site.py https://example.com
    python3 audit_site.py https://example.com --max-pages 8 --json report.json
    python3 audit_site.py https://example.com --no-ai-ua   # 跳过 AI 爬虫 UA 对比
"""

import argparse
import gzip
import json
import re
import ssl
import sys
import urllib.error
import urllib.request
from html.parser import HTMLParser
from typing import Dict, List, Optional
from urllib.parse import urljoin, urlparse

TIMEOUT = 20
BROWSER_UA = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
)
GPTBOT_UA = "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.2; +https://openai.com/gptbot"

AI_BOTS = [
    "GPTBot", "OAI-SearchBot", "ChatGPT-User", "ClaudeBot", "Claude-Web",
    "anthropic-ai", "PerplexityBot", "Google-Extended", "Bytespider",
    "Baiduspider", "YisouSpider", "Applebot-Extended", "CCBot", "meta-externalagent",
]

SKIP_TEXT_TAGS = {"script", "style", "noscript", "svg", "template"}


def fetch(url: str, ua: str = BROWSER_UA, timeout: int = TIMEOUT):
    """返回 (status, headers, body_text),异常时 status 为 None。"""
    req = urllib.request.Request(
        url,
        headers={
            "User-Agent": ua,
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
        },
    )
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    try:
        with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
            raw = resp.read()
            if resp.headers.get("Content-Encoding", "").lower() == "gzip":
                try:
                    raw = gzip.decompress(raw)
                except OSError:
                    pass
            charset = resp.headers.get_content_charset() or "utf-8"
            try:
                text = raw.decode(charset, errors="replace")
            except LookupError:
                text = raw.decode("utf-8", errors="replace")
            return resp.status, dict(resp.headers), text
    except urllib.error.HTTPError as exc:
        return exc.code, dict(exc.headers or {}), ""
    except Exception as exc:  # noqa: BLE001
        return None, {"error": str(exc)}, ""


class PageParser(HTMLParser):
    def __init__(self):
        super().__init__(convert_charrefs=True)
        self.title = ""
        self.meta: Dict[str, str] = {}
        self.h1: List[str] = []
        self.h2: List[str] = []
        self.h3: List[str] = []
        self.jsonld: List[str] = []
        self.canonical: Optional[str] = None
        self.hreflang = 0
        self.imgs = 0
        self.imgs_no_alt = 0
        self.anchors = 0
        self.text_chunks: List[str] = []
        self.ld_json_raw: List[str] = []
        self._stack: List[str] = []
        self._skip_depth = 0
        self._in = None
        self._buf: List[str] = []

    def handle_starttag(self, tag, attrs):
        a = {k.lower(): (v or "") for k, v in attrs}
        self._stack.append(tag)
        if tag in SKIP_TEXT_TAGS:
            self._skip_depth += 1
        elif tag in ("h1", "h2", "h3", "title"):
            self._in = tag
            self._buf = []
        elif tag == "meta":
            key = (a.get("name") or a.get("property") or a.get("http-equiv") or "").lower()
            if key:
                self.meta.setdefault(key, a.get("content", ""))
        elif tag == "img":
            self.imgs += 1
            if not a.get("alt", "").strip():
                self.imgs_no_alt += 1
        elif tag == "a":
            self.anchors += 1
        elif tag == "link":
            rel = a.get("rel", "").lower()
            if "canonical" in rel:
                self.canonical = a.get("href", "")
            if "alternate" in rel and a.get("hreflang"):
                self.hreflang += 1
        elif tag == "script" and a.get("type", "").lower() == "application/ld+json":
            self._in = "ldjson"
            self._buf = []

    def handle_endtag(self, tag):
        if self._stack and self._stack[-1] == tag:
            self._stack.pop()
        if tag in SKIP_TEXT_TAGS and self._skip_depth > 0:
            self._skip_depth -= 1
        if self._in == tag:
            value = " ".join("".join(self._buf).split())
            if tag == "title":
                self.title = value
            elif tag == "h1":
                self.h1.append(value)
            elif tag == "h2":
                self.h2.append(value)
            elif tag == "h3":
                self.h3.append(value)
            self._in = None
            self._buf = []
        elif self._in == "ldjson" and tag == "script":
            self.ld_json_raw.append("".join(self._buf))
            self._in = None
            self._buf = []

    def handle_data(self, data):
        if self._in:
            self._buf.append(data)
            return
        if self._skip_depth:
            return
        stripped = data.strip()
        if stripped:
            self.text_chunks.append(stripped)


def collect_ld_types(blocks: List[str]) -> List[str]:
    types = set()

    def walk(node):
        if isinstance(node, dict):
            t = node.get("@type")
            if isinstance(t, str):
                types.add(t)
            elif isinstance(t, list):
                types.update(x for x in t if isinstance(x, str))
            for v in node.values():
                walk(v)
        elif isinstance(node, list):
            for v in node:
                walk(v)

    for block in blocks:
        block = block.strip()
        if not block:
            continue
        try:
            walk(json.loads(block))
        except json.JSONDecodeError:
            # 容错:抽取 @type 字面量
            types.update(re.findall(r'"@type"\s*:\s*"([^"]+)"', block))
    return sorted(types)


def parse_robots(text: str):
    groups: List[Dict[str, object]] = []
    cur: Optional[Dict[str, object]] = None
    for line in text.splitlines():
        line = line.split("#", 1)[0].strip()
        if not line or ":" not in line:
            continue
        key, val = line.split(":", 1)
        key = key.strip().lower()
        val = val.strip()
        if key == "user-agent":
            if cur is None or cur.get("rules"):
                cur = {"agents": [], "rules": []}
                groups.append(cur)
            cur["agents"].append(val)
        elif key in ("allow", "disallow") and cur is not None:
            cur["rules"].append((key, val))
    return groups


def robots_policy(groups, bot: str):
    """返回 (matched_agent, 是否被整站禁止)。"""
    for g in groups:
        agents = [a.lower() for a in g["agents"]]
        if any(a == bot.lower() for a in agents):
            disallow_all = any(k == "disallow" and v == "/" for k, v in g["rules"])
            allow_all = any(k == "allow" and v == "/" for k, v in g["rules"])
            return g["agents"][0], disallow_all and not allow_all
    for g in groups:
        if any(a == "*" for a in g["agents"]):
            disallow_all = any(k == "disallow" and v == "/" for k, v in g["rules"])
            return "*", disallow_all
    return None, False


def sitemap_urls(base: str, text: str) -> List[str]:
    urls = re.findall(r"<loc>\s*([^<\s]+)\s*</loc>", text)
    if "<sitemapindex" in text:
        out = []
        for sub in urls[:3]:
            status, _, sub_text = fetch(sub)
            if status == 200:
                out.extend(re.findall(r"<loc>\s*([^<\s]+)\s*</loc>", sub_text))
        return out
    return urls


def audit(base: str, max_pages: int, check_ai_ua: bool) -> Dict[str, object]:
    base = base.rstrip("/")
    report: Dict[str, object] = {"site": base, "findings": [], "pages": []}
    findings = report["findings"]

    # --- robots.txt ---
    status, _, robots_txt = fetch(base + "/robots.txt")
    groups = parse_robots(robots_txt) if status == 200 else []
    report["robots"] = {
        "status": status,
        "groups": len(groups),
        "ai_bots": {bot: robots_policy(groups, bot)[1] if groups else None for bot in AI_BOTS},
        "sitemap_declared": bool(re.search(r"(?im)^sitemap:", robots_txt or "")),
    }
    if status != 200:
        findings.append(("P0", "robots.txt 缺失", "补一份,并显式声明各 AI 爬虫的允许策略与 sitemap 地址"))
    else:
        blocked = [b for b, v in report["robots"]["ai_bots"].items() if v is True]
        if blocked:
            findings.append(("P1", "以下 AI 爬虫被整站禁止: " + ", ".join(blocked), "确认是否为预期策略;误拦会让品牌在对应引擎中不可见"))
        if not report["robots"]["sitemap_declared"]:
            findings.append(("P2", "robots.txt 未声明 Sitemap", "加一行 Sitemap: <绝对地址>"))

    # --- llms.txt ---
    status, _, llms = fetch(base + "/llms.txt")
    report["llms_txt"] = {"status": status, "bytes": len(llms or "")}
    if status != 200:
        findings.append(("P2", "llms.txt 缺失", "按 references/ai-crawler-policy.md 的模板补站点摘要与关键页面索引"))

    # --- sitemap ---
    status, _, sm = fetch(base + "/sitemap.xml")
    urls = sitemap_urls(base, sm) if status == 200 else []
    report["sitemap"] = {"status": status, "urls": len(urls)}
    if status != 200 or not urls:
        findings.append(("P1", "sitemap.xml 不可用或为空", "生成分区 sitemap,lastmod 用真实更新时间"))

    # --- 待检页面 ---
    targets = [base + "/"]
    for u in urls:
        if len(targets) >= max_pages + 1:
            break
        if u.rstrip("/") == base:
            continue
        targets.append(u)

    all_types = set()
    for url in targets:
        status, headers, html = fetch(url)
        if status != 200 or not html:
            report["pages"].append({"url": url, "status": status})
            continue
        p = PageParser()
        try:
            p.feed(html)
        except Exception:  # noqa: BLE001
            pass
        body_text = " ".join(p.text_chunks)
        ld_types = collect_ld_types(p.ld_json_raw)
        all_types.update(ld_types)
        report["pages"].append({
            "url": url,
            "status": status,
            "bytes": len(html),
            "text_chars": len(body_text),
            "title_len": len(p.title),
            "desc_len": len(p.meta.get("description", "")),
            "h1": len(p.h1),
            "h2": len(p.h2),
            "h1_text": p.h1[0][:80] if p.h1 else "",
            "jsonld_types": ld_types,
            "canonical": bool(p.canonical),
            "hreflang": p.hreflang,
            "img_no_alt": p.imgs_no_alt,
            "img_total": p.imgs,
            "empty_root": bool(re.search(r'<div[^>]+id="(root|app|__next)"[^>]*>\s*</div>', html)),
        })

    # --- 页面级问题 ---
    pages = [x for x in report["pages"] if x.get("status") == 200]
    if pages:
        no_h1 = [x["url"] for x in pages if x["h1"] == 0]
        multi_h1 = [x["url"] for x in pages if x["h1"] > 1]
        thin = [x["url"] for x in pages if x["text_chars"] < 600]
        empty_root = [x["url"] for x in pages if x["empty_root"] and x["text_chars"] < 300]
        no_ld = [x["url"] for x in pages if not x["jsonld_types"]]
        no_canon = [x["url"] for x in pages if not x["canonical"]]
        no_desc = [x["url"] for x in pages if x["desc_len"] < 30]
        img_gap = sum(x["img_no_alt"] for x in pages)

        if empty_root:
            findings.append(("P0", "检测到纯客户端渲染迹象: " + ", ".join(empty_root[:3]), "改为 SSR/SSG,AI 爬虫普遍不执行 JS,正文等于不存在"))
        if no_h1:
            findings.append(("P0", "缺 H1 的页面: " + ", ".join(no_h1[:5]), "每页一个 H1,写成能独立回答问题的陈述句"))
        if multi_h1:
            findings.append(("P1", f"{len(multi_h1)} 个页面存在多个 H1", "收敛为一个,其余降级为 H2"))
        if thin:
            findings.append(("P1", f"{len(thin)} 个页面正文过短(<600 字): " + ", ".join(thin[:3]), "补足定义段、适用对象、交付物、限制条件"))
        if no_ld:
            findings.append(("P1", f"{len(no_ld)} 个页面无 JSON-LD", "按 references/schema-templates.md 注入 Organization/Article/FAQPage 等"))
        if no_canon:
            findings.append(("P2", f"{len(no_canon)} 个页面缺 canonical", "统一规范地址,避免 AI 侧把参数页当重复内容"))
        if no_desc:
            findings.append(("P2", f"{len(no_desc)} 个页面 meta description 过短或缺失", "写 80-140 字,含实体名与一句能力定义"))
        if img_gap:
            findings.append(("P2", f"{img_gap} 张图片缺 alt", "alt 写成名词短语,关键数据不要只存在于图片里"))
        hreflang_pages = [x for x in pages if x["hreflang"] > 0]
        if hreflang_pages and len(hreflang_pages) < len(pages):
            findings.append(("P1", "hreflang 覆盖不完整", "所有语言版本互相声明且闭合,否则各语言页在 AI 侧互相镜像"))

    # --- 实体层结构化数据 ---
    entity_types = {"Organization", "LocalBusiness", "Corporation", "Brand", "Person"}
    report["entity_schema"] = sorted(all_types & entity_types)
    if not report["entity_schema"]:
        findings.append(("P0", "全站未发现 Organization/LocalBusiness 结构化数据", "补组织实体节点,用 @id 与文章 publisher 串联"))

    # --- AI 爬虫 UA 对比 ---
    if check_ai_ua:
        b_status, b_headers, b_html = fetch(base + "/")
        g_status, g_headers, g_html = fetch(base + "/", ua=GPTBOT_UA)
        bp, gp = PageParser(), PageParser()
        try:
            bp.feed(b_html)
            gp.feed(g_html)
        except Exception:  # noqa: BLE001
            pass
        report["ai_ua_probe"] = {
            "browser_status": b_status,
            "gptbot_status": g_status,
            "browser_chars": len(" ".join(bp.text_chunks)),
            "gptbot_chars": len(" ".join(gp.text_chunks)),
        }
        if b_status != g_status or abs(len(" ".join(bp.text_chunks)) - len(" ".join(gp.text_chunks))) > 200:
            findings.append(("P1", "对 GPTBot 与浏览器的响应不一致", "确认是否 waf/反爬误拦 AI 爬虫"))

    order = {"P0": 0, "P1": 1, "P2": 2}
    findings.sort(key=lambda x: order.get(x[0], 9))
    report["summary"] = {k: sum(1 for f in findings if f[0] == k) for k in ("P0", "P1", "P2")}
    return report


def main():
    ap = argparse.ArgumentParser(description="GEO 官网体检")
    ap.add_argument("url")
    ap.add_argument("--max-pages", type=int, default=5)
    ap.add_argument("--json", dest="json_out")
    ap.add_argument("--no-ai-ua", action="store_true")
    args = ap.parse_args()

    report = audit(args.url, args.max_pages, not args.no_ai_ua)

    print("=" * 62)
    print("GEO 体检报告  " + report["site"])
    print("=" * 62)
    print("robots.txt : " + str(report["robots"]["status"]))
    print("llms.txt   : " + str(report["llms_txt"]["status"]))
    print("sitemap    : " + str(report["sitemap"]["status"]) + " / " + str(report["sitemap"]["urls"]) + " 条")
    print("实体 schema: " + (", ".join(report["entity_schema"]) or "无"))
    if report.get("ai_ua_probe"):
        print("GPTBot 探测 : " + json.dumps(report["ai_ua_probe"], ensure_ascii=False))

    print("\n-- 页面概览 --")
    for p in report["pages"]:
        if p.get("status") != 200:
            print(f"  [{p.get('status')}] {p['url']}")
            continue
        print(f"  {p['url']}\n    正文 {p['text_chars']} 字 / H1 {p['h1']} / H2 {p['h2']} / schema {','.join(p['jsonld_types']) or '无'}")

    print("\n-- 改造项 --")
    if not report["findings"]:
        print("  未发现明显问题")
    for level, issue, action in report["findings"]:
        print(f"  [{level}] {issue}\n         → {action}")
    print("\n汇总: P0 %d / P1 %d / P2 %d" % (
        report["summary"]["P0"], report["summary"]["P1"], report["summary"]["P2"]))

    if args.json_out:
        with open(args.json_out, "w", encoding="utf-8") as fh:
            json.dump(report, fh, ensure_ascii=False, indent=2)
        print("JSON 已写入 " + args.json_out)

    return 0 if report["summary"]["P0"] == 0 else 1


if __name__ == "__main__":
    sys.exit(main())
```

팁:scripts/audit_site.py Python 3.8 이상이 필요하며, 서드파티 라이브러리 설치가 필요 없습니다. 스크립트는 P0 문제가 감지되면 종료 코드 1로 종료되며, CI에 바로 연결할 수 있습니다.

이 글은 《중국어 비유료 마케팅 100문》 시리즈의 일부입니다.전체 목차 보기 →
QR
QR 코드 스캔하여 팔로우