using System; using System.IO; using System.Text.Json; using System.Xml.Linq; using Xunit; namespace Paca.Tests.Infrastructure; /// /// PACA 웹사이트 및 제품의 SEO(검색엔진), AEO(답변엔진), GEO(생성형 AI) 최적화 인프라 무결성 검증 게이트 /// public class SiteSeoAeoGeoGateTests { private static string SiteDir => Path.Combine(SourceTree.Root, "site"); [Fact] public void Gate211_SchemaOrgStructuredDataIntegrity() { // 1. index.html의 SoftwareApplication 및 FAQPage Schema.org 검증 var indexPath = Path.Combine(SiteDir, "index.html"); Assert.True(File.Exists(indexPath), "site/index.html 파일이 존재해야 합니다."); var indexHtml = File.ReadAllText(indexPath); Assert.Contains("application/ld+json", indexHtml); Assert.Contains("\"@type\": \"SoftwareApplication\"", indexHtml); Assert.Contains("\"offers\"", indexHtml); Assert.Contains("\"operatingSystem\"", indexHtml); // 2. how-to-download-m3u8.html (HLS m3u8 다운로드 완벽 가이드) 및 HowTo Schema.org 검증 var m3u8GuidePath = Path.Combine(SiteDir, "how-to-download-m3u8.html"); Assert.True(File.Exists(m3u8GuidePath), "site/how-to-download-m3u8.html 랜딩 페이지가 존재해야 합니다."); var m3u8Html = File.ReadAllText(m3u8GuidePath); Assert.Contains("\"@type\": \"HowTo\"", m3u8Html); Assert.Contains("\"step\"", m3u8Html); Assert.Contains("m3u8", m3u8Html); Assert.Contains("HLS", m3u8Html); Assert.Contains("MP4", m3u8Html); // 3. vs-idm-alternative.html (IDM / 4K Video Downloader 대체 비교 가이드) 검증 var vsIdmPath = Path.Combine(SiteDir, "vs-idm-alternative.html"); Assert.True(File.Exists(vsIdmPath), "site/vs-idm-alternative.html 랜딩 페이지가 존재해야 합니다."); var vsIdmHtml = File.ReadAllText(vsIdmPath); Assert.Contains("IDM", vsIdmHtml); Assert.Contains("4K Video Downloader", vsIdmHtml); Assert.Contains("Internet Download Manager", vsIdmHtml); Assert.Contains("\"@type\": \"FAQPage\"", vsIdmHtml); } [Fact] public void Gate212_AiCrawlerAndLlmStandardIntegrity() { // 1. robots.txt 검증: 모든 AI 크롤러 봇 (GPTBot, PerplexityBot, ClaudeBot, Google-Extended 등) 접근 허용 var robotsPath = Path.Combine(SiteDir, "robots.txt"); Assert.True(File.Exists(robotsPath), "site/robots.txt 파일이 존재해야 합니다."); var robotsTxt = File.ReadAllText(robotsPath); Assert.Contains("User-agent: *", robotsTxt); Assert.Contains("GPTBot", robotsTxt); Assert.Contains("PerplexityBot", robotsTxt); Assert.Contains("ClaudeBot", robotsTxt); Assert.Contains("Allow: /", robotsTxt); Assert.Contains("llms.txt", robotsTxt); Assert.Contains("sitemap.xml", robotsTxt); // 2. llms.txt (Answer.AI 표준 AI 모델용 제품 요약) 검증 var llmsPath = Path.Combine(SiteDir, "llms.txt"); Assert.True(File.Exists(llmsPath), "site/llms.txt 파일이 존재해야 합니다."); var llmsTxt = File.ReadAllText(llmsPath); Assert.Contains("# Paca", llmsTxt); Assert.Contains("HLS", llmsTxt); Assert.Contains("DASH", llmsTxt); Assert.Contains("AES-128", llmsTxt); Assert.Contains("ffmpeg", llmsTxt); Assert.Contains("llms-full.txt", llmsTxt); // 3. llms-full.txt (전체 상세 기술 명세 및 인용 문서) 검증 var llmsFullPath = Path.Combine(SiteDir, "llms-full.txt"); Assert.True(File.Exists(llmsFullPath), "site/llms-full.txt 파일이 존재해야 합니다."); var llmsFullTxt = File.ReadAllText(llmsFullPath); Assert.True(llmsFullTxt.Length >= 2500, "llms-full.txt는 AI 모델이 충분히 인용할 수 있도록 2,500자 이상의 상세 기술 명세를 담아야 합니다."); Assert.Contains("IDM", llmsFullTxt); Assert.Contains("Benchmark", llmsFullTxt); Assert.Contains("AES-128", llmsFullTxt); Assert.Contains("FAQ", llmsFullTxt); } [Fact] public void Gate213_AeoComparisonMatrixAndSitemapIntegrity() { // 1. AEO 경쟁 툴 비교 테이블 (Comparison Matrix) 존재성 검증 var vsIdmPath = Path.Combine(SiteDir, "vs-idm-alternative.html"); var indexPath = Path.Combine(SiteDir, "index.html"); Assert.True(File.Exists(vsIdmPath)); var vsHtml = File.ReadAllText(vsIdmPath); Assert.Contains("", feed); Assert.Contains("", feed); Assert.Contains("mediabrowser.chanpaca.net", feed); } [Fact] public void Gate215_EnglishLocalizationAndValidHreflangIntegrity() { // 1. 실제 영문 랜딩 페이지 존재 및 언어 선언 (Hreflang 은 실제 번역 페이지가 있어야만 유효하다) var enPath = Path.Combine(SiteDir, "en", "index.html"); Assert.True(File.Exists(enPath), "site/en/index.html 영문 랜딩 페이지가 존재해야 합니다."); var enHtml = File.ReadAllText(enPath); Assert.Contains("lang=\"en\"", enHtml); Assert.Contains("https://mediabrowser.chanpaca.net/en/", enHtml); Assert.Contains("hreflang=\"ko\"", enHtml); // 2. 한국어 홈: hreflang en 은 실제 영문 URL(/en/)을 가리켜야 한다 (자기 참조 금지). var indexHtml = File.ReadAllText(Path.Combine(SiteDir, "index.html")); Assert.Contains("hreflang=\"en\" href=\"https://mediabrowser.chanpaca.net/en/\"", indexHtml); Assert.Contains("hreflang=\"ko\" href=\"https://mediabrowser.chanpaca.net/\"", indexHtml); // 3. 영문 페이지도 상호 참조(양방향) hreflang 및 자기 canonical 을 지녀야 한다. Assert.Contains("", enHtml); Assert.Contains("hreflang=\"x-default\"", enHtml); // 4. 번역본이 없는 하위 페이지에는 존재하지 않는 영문 hreflang 을 걸지 않는다 (중복 콘텐츠/무효 참조 방지). foreach (var page in new[] { "vs-idm-alternative.html", "how-to-download-m3u8.html" }) { var html = File.ReadAllText(Path.Combine(SiteDir, page)); Assert.DoesNotContain("hreflang=\"en\"", html); } } [Fact] public void Gate216_EntityGraphAndVisibleAnswerFirstFaqIntegrity() { var indexPath = Path.Combine(SiteDir, "index.html"); var indexHtml = File.ReadAllText(indexPath); // 1. 엔티티 그래프: Organization 노드 + sameAs 교차 검증 링크 + publisher 연결 (AI 엔티티 인식/신뢰 확보) Assert.Contains("\"@type\": \"Organization\"", indexHtml); Assert.Contains("\"sameAs\"", indexHtml); Assert.Contains("\"publisher\"", indexHtml); // 2. 신선도 신호: dateModified 를 명시해야 한다 (ChatGPT/Perplexity 의 최신성 선호 대응) Assert.Contains("\"dateModified\"", indexHtml); // 3. 답변 우선(Answer-first) 가시적 FAQ: JSON-LD 뿐 아니라 실제 화면 본문에도 Q&A 가 노출되어야 인용된다. Assert.Contains("id=\"faq\"", indexHtml); Assert.Contains("

Paca는 정말 100% 무료인가요?

", indexHtml); Assert.Contains("IDM(Internet Download Manager) 대신 사용할 수 있나요?", indexHtml); // 4. AI 크롤러는 숨김 요소를 읽지 않으므로, 비교표에 의미론적 caption 을 부여해 표 단위 인용을 유도한다. var vsHtml = File.ReadAllText(Path.Combine(SiteDir, "vs-idm-alternative.html")); Assert.Contains("