const http = require('http'); async function auditPage(url, pageName) { try { const res = await fetch(url); if (!res.ok) { console.log(`[${pageName}] HTTP Status: ${res.status}`); return; } const html = await res.text(); console.log(`\n==============================================`); console.log(`πŸ” [μ •λ°€ SEO 감사 λ³΄κ³ μ„œ] ${pageName}`); console.log(`🌐 λŒ€μƒ URL: ${url}`); console.log(`==============================================`); // 1. Title const titleMatch = html.match(/([^<]+)<\/title>/i); const title = titleMatch ? titleMatch[1].trim() : '❌ μ—†μŒ'; console.log(`1. <title>`); console.log(` - ν…μŠ€νŠΈ: "${title}"`); console.log(` - κΈ€μžμˆ˜: ${title.length}자 (넀이버/ꡬ글 검색 졜적 ꢌμž₯ 30~35자 λ‚΄μ™Έ)`); console.log(` - 검색결과 λ§μ€„μž„(…) λ°©μ§€ 평가: ${title.length >= 25 && title.length <= 40 ? 'βœ… 졜적 (λ§μ€„μž„ μ—†μŒ)' : '⚠️ 점검 ꢌμž₯'}`); // 2. H1 νƒœκ·Έ const h1Matches = [...html.matchAll(/<h1[^>]*>([\s\S]*?)<\/h1>/gi)]; console.log(`\n2. <h1> νƒœκ·Έ`); console.log(` - 발견된 H1 개수: ${h1Matches.length}개 (검색엔진 ꢌμž₯ 단독 1개 원칙)`); h1Matches.forEach((m, i) => { const cleanH1 = m[1].replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); console.log(` - H1 #${i + 1}: "${cleanH1}"`); }); console.log(` - 평가: ${h1Matches.length === 1 ? 'βœ… 단독 H1 규격 μ€€μˆ˜ (검색 κ°€μ€‘μΉ˜ 집쀑)' : '⚠️ H1 쀑볡 λ˜λŠ” λˆ„λ½'}`); // 3. Meta Description const descMatch = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["']/i) || html.match(/<meta[^>]*content=["']([^"']+)["'][^>]*name=["']description["']/i); const desc = descMatch ? descMatch[1].trim() : '❌ μ—†μŒ'; console.log(`\n3. Meta Description`); console.log(` - λ‚΄μš©: "${desc}"`); console.log(` - κΈ€μžμˆ˜: ${desc.length}자 (ꢌμž₯ 80~140자)`); console.log(` - 검색 μŠ€λ‹ˆνŽ« CTR 평가: ${desc.length >= 50 && desc.length <= 160 ? 'βœ… 졜적' : '⚠️ 점검 ꢌμž₯'}`); // 4. Canonical URL const canonMatch = html.match(/<link[^>]*rel=["']canonical["'][^>]*href=["']([^"']+)["']/i); const canonical = canonMatch ? canonMatch[1] : '❌ μ—†μŒ'; console.log(`\n4. Canonical URL (ν‘œμ€€ URL)`); console.log(` - λŒ€μƒ URL: ${canonical}`); console.log(` - 평가: ${canonical.startsWith('http') ? 'βœ… μ ˆλŒ€κ²½λ‘œ ν‘œμ€€ μ€€μˆ˜ (쀑볡 λ¬Έμ„œ νŽ˜λ„ν‹° λ°©μ§€)' : '❌ μƒλŒ€κ²½λ‘œ λ˜λŠ” λˆ„λ½'}`); // 5. Naver Site Verification const naverMatch = html.match(/<meta[^>]*name=["']naver-site-verification["'][^>]*content=["']([^"']+)["']/i); console.log(`\n5. 넀이버 μ„œμΉ˜μ–΄λ“œλ°”μ΄μ € μ†Œμœ ν™•μΈ νƒœκ·Έ`); console.log(` - ν‚€ κ°’: ${naverMatch ? naverMatch[1] : '❌ μ—†μŒ'}`); // 6. Open Graph & Twitter Cards const ogTitle = (html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["']/i) || [])[1]; const ogImage = (html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']+)["']/i) || [])[1]; const twitterCard = (html.match(/<meta[^>]*name=["']twitter:card["'][^>]*content=["']([^"']+)["']/i) || [])[1]; console.log(`\n6. μ†Œμ…œ λ―Έλ””μ–΄ 메타데이터 (OG & Twitter)`); console.log(` - og:title: ${ogTitle || '❌'}`); console.log(` - og:image: ${ogImage || '❌'}`); console.log(` - twitter:card: ${twitterCard || '❌'}`); // 7. JSON-LD ꡬ쑰화 데이터 const ldJsonMatches = [...html.matchAll(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)]; console.log(`\n7. JSON-LD Schema.org ꡬ쑰화 데이터`); console.log(` - μ‚½μž…λœ μŠ€ν‚€λ§ˆ 개수: ${ldJsonMatches.length}개`); ldJsonMatches.forEach((m, i) => { try { const parsed = JSON.parse(m[1]); const type = parsed['@type'] ? (Array.isArray(parsed['@type']) ? parsed['@type'].join(', ') : parsed['@type']) : 'Unknown'; console.log(` - μŠ€ν‚€λ§ˆ #${i + 1} νƒ€μž…: ${type} (βœ… JSON ꡬ문 유효)`); } catch (e) { console.log(` - μŠ€ν‚€λ§ˆ #${i + 1}: ❌ JSON νŒŒμ‹± μ—λŸ¬ (${e.message})`); } }); // 8. 이미지 νƒœκ·Έ 점검 (alt, width, height, ν•œκΈ€ 파일λͺ…) const imgMatches = [...html.matchAll(/<img([^>]+)>/gi)]; console.log(`\n8. 이미지 (<img>) SEO μ΅œμ ν™” 점검`); console.log(` - 총 이미지 νƒœκ·Έ 수: ${imgMatches.length}개`); let missingAlt = 0; let hasKoreanSrc = 0; imgMatches.forEach(m => { const attrs = m[1]; if (!attrs.includes('alt=') || attrs.includes('alt=""')) missingAlt++; const srcMatch = attrs.match(/src=["']([^"']+)["']/); if (srcMatch && /[γ„±-γ…Ž|ㅏ-γ…£|κ°€-힣]/.test(srcMatch[1])) hasKoreanSrc++; }); console.log(` - alt λˆ„λ½ 이미지: ${missingAlt}개`); console.log(` - ν•œκΈ€ 파일λͺ… src 이미지: ${hasKoreanSrc}개`); console.log(` - 평가: ${missingAlt === 0 && hasKoreanSrc === 0 ? 'βœ… 100% 졜적 (크둀러 인코딩 였λ₯˜ 0건)' : '⚠️ 일뢀 점검 ν•„μš”'}`); // 9. λ‘œλ΄‡ μˆ˜μ§‘ 차단 λ°©ν•΄ μš”μ†Œ 점검 (noindex, nofollow) const robotsMeta = (html.match(/<meta[^>]*name=["']robots["'][^>]*content=["']([^"']+)["']/i) || [])[1]; console.log(`\n9. 크둀러 λ°©ν•΄ μš”μ†Œ 점검 (Crawl Obstacles)`); console.log(` - robots λ©”νƒ€νƒœκ·Έ: ${robotsMeta || 'μ„€μ • μ—†μŒ (κΈ°λ³Έ index, follow ν—ˆμš©)'}`); if (robotsMeta && robotsMeta.includes('noindex')) { console.log(` - 🚨 치λͺ…적: noindex둜 인해 검색엔진 μˆ˜μ§‘μ΄ μ°¨λ‹¨λ©λ‹ˆλ‹€!`); } else { console.log(` - μˆ˜μ§‘ 차단 μ—¬λΆ€: βœ… 정상 (검색엔진 차단 λ°©ν•΄ μš”μ†Œ μ—†μŒ)`); } } catch (err) { console.error(`Error auditing ${url}:`, err.message); } } (async () => { // 포트 3015(λΆ€μ‚° μ„œλ²„) 점검 await auditPage('http://localhost:3015/', 'λΆ€μ‚° 메인 ν™ˆνŽ˜μ΄μ§€'); await auditPage('http://localhost:3015/%EC%9A%B0%EB%8F%99-%EC%A4%91%ED%95%99%EC%83%9D-%EC%88%98%ED%95%99', 'λΆ€μ‚° μ§€μ—­ λ‘±ν…ŒμΌ 상세 (μš°λ™ 쀑학생 μˆ˜ν•™)'); // 포트 3011(site1) 점검 μ‹œλ„ await auditPage('http://localhost:3011/', 'μ„œμšΈ site1 메인 ν™ˆνŽ˜μ΄μ§€'); })();