diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d250b..babaa27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning] +## [2.1.0] - 2026-03-18 22:06 UTC + +### Added +- **Comprehensive 17-Section Report** — Full rebuild of SEO/GEO audit report generator with all industry-standard sections: + - Executive Summary with stats grid + - 6-Category Scoring Breakdown with progress bars (Overall, Technical, Content, Performance, Social, GEO) + - Meta Tags Analysis (9 checks: title, description, keywords, viewport, charset, canonical, robots, X-Frame-Options, protocol) + - Social & Open Graph (8 properties with pass/fail status) + - Heading Structure with hierarchy visualization + - Links Analysis with sample external links table + - Images & Alt Text with missing-alt report + - Content Analysis with readability recommendations + - Performance Signals (12 metrics including encoding, scripts, preconnect, preload, DNS prefetch, async/defer) + - Accessibility (4 checks: lang, ARIA, first-image alt, alt coverage) + - Structured Data / Schema detection + - Hreflang Tags + - GEO Analysis with readiness score, 4 sub-scores (Factual, Entity, Content Depth, Citeability), and improvement checklist + - Issues & How to Fix (severity badges, category-specific fix instructions) + - Prioritized Action Plan (high/medium/low with impact labels) + - Recommended FAQ Schema Questions (auto-generated based on domain) + - SEO/GEO Best Practices Checklist (12 items) +- **GEO Scoring Engine** — 0-100 GEO readiness score based on schema markup, content depth, heading hierarchy, and citeability signals +- **Auto-Generated Action Plan** — 20+ prioritized fixes derived from audit data with severity and impact labels +- **Print-Optimized CSS** — Clean white-on-dark print styles for professional PDF output + +### Technical Details +- Files modified: 1 (seo-report.ts) — complete rewrite, 530+ lines + + ## [2.0.1] - 2026-03-18 21:56 UTC ### Fixed diff --git a/README.md b/README.md index 944517b..7d6bc8f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # PromptArch: AI Orchestration Platform -> **Latest Version**: [v2.0.1](CHANGELOG.md#2.0.1---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.0---2026-03-18) (2026-03-18)(CHANGELOG.md#190---2026-03-18) (2026-03-18) +> **Latest Version**: [v2.1.0](CHANGELOG.md#2.1.0---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.1---2026-03-18) (2026-03-18)(CHANGELOG.md#2.0.0---2026-03-18) (2026-03-18)(CHANGELOG.md#190---2026-03-18) (2026-03-18) > **Development Note**: This entire platform was developed exclusively using [TRAE.AI IDE](https://trae.ai) powered by elite [GLM 4.7 model](https://z.ai/subscribe?ic=R0K78RJKNW). > **Learn more about this architecture [here](https://z.ai/subscribe?ic=R0K78RJKNW).** @@ -143,6 +143,7 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | Version | Date | Highlights | |---------|------|------------| +| [2.1.0](CHANGELOG.md#2.1.0---2026-03-18) | Full 17-Section Report, GEO Scoring, Action Plan, FAQ Gen | 2026-03-18 | | [2.0.1](CHANGELOG.md#2.0.1---2026-03-18) | Inline SEO Export, PDF Print Fix | 2026-03-18 | | [2.0.0](CHANGELOG.md#2.0.0---2026-03-18) | SEO Export, Default Vibe, /vibe Route, General Chat | 2026-03-18 | | [1.9.0](CHANGELOG.md#190---2026-03-18) | 2026-03-18 21:05 UTC | Vibe Architect rebrand, sidebar highlight | diff --git a/lib/seo-report.ts b/lib/seo-report.ts index b1021a3..ae02917 100644 --- a/lib/seo-report.ts +++ b/lib/seo-report.ts @@ -1,265 +1,573 @@ -/** - * SEO Audit Report Generator - * Generates standalone HTML reports from audit data - */ - -export interface SeoAuditData { - url: string; - domain: string; - responseTime: number; - server: string; - htmlSize: number; - title: string | null; - titleLength: number; - titleStatus: string; - metaDescription: string | null; - descLength: number; - descStatus: string; - metaKeywords: string | null; - viewport: string | null; - charset: string | null; - robotsDirectives: string | null; - canonical: string | null; - hasCanonicalMismatch: boolean; - h1Count: number; - h2Count: number; - h3Count: number; - h4Count: number; - headingStatus: string; - headings: { level: number; text: string }[]; - links?: { total: number; internal: number; external: number; nofollow: number }; - images?: { total: number; withAlt: number; withoutAlt: number; lazyLoaded: number; altCoverage: number }; - content?: { wordCount: number; sentenceCount: number; paragraphCount: number; avgWordsPerSentence: number }; - openGraph?: { title: string | null; description: string | null; image: string | null; type: string | null }; - twitterCard?: { card: string | null }; - performance?: { - inlineStyles: number; - externalScripts: number; - externalStylesheets: number; - hasPreconnect: boolean; - hasPreload: boolean; - hasDnsPrefetch: boolean; - usesAsyncScripts: boolean; - usesDeferScripts: boolean; - }; - accessibility?: { hasLangAttr: boolean; hasAriaLabels: boolean; hasAltOnFirstImage: boolean }; - structuredData?: { hasJsonLd: boolean; hasMicrodata: boolean; types: { type: string; found: boolean }[] }; - scores?: { overall: number; technical: number; content: number; performance: number; social: number }; - issues?: { severity: string; category: string; message: string }[]; -} - -const scoreColor = (s: number): string => - s >= 80 ? "#22c55e" : s >= 60 ? "#f59e0b" : "#ef4444"; - -const sevColor = (s: string): string => - s === "critical" ? "#ef4444" : s === "warning" ? "#f59e0b" : "#6b7280"; - -const fixInstructions: Record = { - Meta: "Ensure every page has a unique title tag (50-60 chars) with primary keyword near start. Write meta descriptions (150-160 chars) with CTA. Add viewport meta tag.", - Content: "Maintain exactly one H1 per page with primary keyword. Build logical heading hierarchy (H1>H2>H3). Aim for 1000+ words on core pages.", - Technical: "Set self-referencing canonical tags. Ensure valid SSL with HTTPS redirect. Check robots.txt is not blocking important pages.", - Mobile: "Add viewport meta tag. Test with Google Mobile-Friendly tool. Ensure tap targets are 48x48px minimum.", - Security: "Migrate to HTTPS with valid SSL. Set up HTTP-to-HTTPS redirects. Configure HSTS headers.", - Performance: "Minimize inline styles. Add preconnect hints. Implement lazy loading. Use async/defer for scripts. Compress images.", - Social: "Add OG tags (og:title, og:description, og:image). Add Twitter Card meta. Ensure OG images are 1200x630px+.", - Accessibility: "Add lang attribute to . Implement ARIA labels. Add descriptive alt text to all images.", - Links: "Fix broken internal links. Add external links to authoritative sources. Review nofollow attributes.", - "Structured Data": "Implement JSON-LD schema. Validate with Google Rich Results Test. Add FAQ/Article/Product schema.", -}; - -export function generateSeoReportHtml(d: SeoAuditData): string { - const now = new Date().toLocaleString(); - const sc = scoreColor; - const svc = sevColor; - - const issueRows = (d.issues || []) - .map( - (issue) => - "" + - issue.severity.toUpperCase() + - "" + - issue.category + - "" + - issue.message + - "" + - (fixInstructions[issue.category] || "Review and address this issue.") + - "" - ) - .join(""); - - const headingRows = (d.headings || []) - .map((h) => "H" + h.level + "" + h.text + "") - .join(""); - - const t = (v: string | null | undefined, fallback = "None") => v || "" + fallback + ""; - - return "SEO Audit - " + - (d.domain || "report") + - "

SEO/GEO Audit Report

" + - (d.url || "N/A") + - "

PromptArch Vibe Architect | " + - now + - "

" + - (d.scores?.overall || 0) + - "
Overall
" + - (d.scores?.technical || 0) + - "
Technical
" + - (d.scores?.content || 0) + - "
Content
" + - (d.scores?.performance || 0) + - "
Performance
" + - (d.scores?.social || 0) + - "
Social
" + - // Meta Tags - "

Meta Tags

ElementValueStatus
Title" + - t(d.title, "MISSING") + - " (" + - (d.titleLength || 0) + - " chars)" + - (d.titleStatus || "?") + - "
Description" + - t(d.metaDescription, "MISSING") + - " (" + - (d.descLength || 0) + - " chars)" + - (d.descStatus || "?") + - "
Viewport" + - t(d.viewport, "MISSING") + - "" + - (d.viewport ? "OK" : "Missing") + - "
Canonical" + - (d.canonical || "None") + - "" + - (d.hasCanonicalMismatch ? "MISMATCH" : d.canonical ? "OK" : "Missing") + - "
Robots" + - (d.robotsDirectives || "None") + - "
" + - // Social - "

Social / Open Graph

PropertyValue
OG Title" + - (d.openGraph?.title || "None") + - "
OG Description" + - (d.openGraph?.description || "None") + - "
OG Image" + - (d.openGraph?.image || "None") + - "
OG Type" + - (d.openGraph?.type || "None") + - "
Twitter Card" + - (d.twitterCard?.card || "None") + - "
" + - // Headings - "

Headings

H1: " + - d.h1Count + - " | H2: " + - d.h2Count + - " | H3: " + - d.h3Count + - " | H4: " + - d.h4Count + - " [" + - (d.headingStatus || "?") + - "]

" + - (headingRows ? "" + headingRows + "
LevelText
" : "") + - "
" + - // Links & Images - "

Links & Images

MetricValue
Total Links" + - (d.links?.total || 0) + - "
Internal" + - (d.links?.internal || 0) + - "
External" + - (d.links?.external || 0) + - "
Nofollow" + - (d.links?.nofollow || 0) + - "
Images" + - (d.images?.total || 0) + - " (" + - (d.images?.altCoverage || 0) + - "% alt)
Missing Alt" + - (d.images?.withoutAlt || 0) + - "
Lazy Loaded" + - (d.images?.lazyLoaded || 0) + - "
" + - // Content - "

Content

MetricValue
Words" + - (d.content?.wordCount || 0) + - "
Sentences" + - (d.content?.sentenceCount || 0) + - "
Avg Words/Sentence" + - (d.content?.avgWordsPerSentence || 0) + - "
" + - // Performance - "

Performance

MetricValue
Server" + - (d.server || "Unknown") + - "
Response Time" + - d.responseTime + - "ms
HTML Size" + - (d.htmlSize || 0).toLocaleString() + - " bytes
External Scripts" + - (d.performance?.externalScripts || 0) + - "
Inline Styles" + - (d.performance?.inlineStyles || 0) + - "
Preconnect" + - (d.performance?.hasPreconnect ? "Yes" : "No") + - "
Async/Defer" + - (d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts ? "Yes" : "No") + - "
" + - // Issues - "

Issues & How to Fix

" + - (d.issues?.length || 0) + - " issues found

" + - (issueRows || "") + - "
SeverityCategoryIssueHow to Fix
No issues.
" + - // Recommendations - "

SEO/GEO Recommendations

  1. Title Tags: Keep under 60 chars, primary keyword near start, unique per page.
  2. Meta Descriptions: 150-160 chars with CTA, include target keywords naturally.
  3. Headings: One H1 per page, logical hierarchy, keywords in headings.
  4. Content: 1000+ words on core pages, authoritative, regularly updated.
  5. Images: Descriptive alt text, WebP format, lazy loading, compressed.
  6. Page Speed: Minimize render-blocking resources, enable compression, use CDN.
  7. Mobile: Responsive design, 48x48px tap targets, no horizontal scroll.
  8. Structured Data: JSON-LD schema, validate with Google Rich Results Test.
  9. Internal Linking: Logical architecture, descriptive anchors, 3-click rule.
  10. Technical: XML sitemap, robots.txt, fix broken links, canonical tags.
  11. GEO: Structure content with clear facts/lists/tables. Use schema for entities. Optimize for featured snippets. Create authoritative content AI models can cite.

PromptArch Vibe Architect | " + - now + - "

rommark.dev/tools/promptarch

"; -} - -export function downloadSeoReport(d: SeoAuditData, format: "html" | "pdf") { - const html = generateSeoReportHtml(d); - - if (format === "html") { - const blob = new Blob([html], { type: "text/html" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "seo-audit-" + (d.domain || "report") + "-" + new Date().toISOString().slice(0, 10) + ".html"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } else { - const iframe = document.createElement("iframe"); - iframe.style.position = "fixed"; - iframe.style.right = "0"; - iframe.style.bottom = "0"; - iframe.style.width = "0"; - iframe.style.height = "0"; - iframe.style.border = "none"; - document.body.appendChild(iframe); - const doc = iframe.contentDocument || iframe.contentWindow?.document; - if (doc) { - doc.open(); - doc.write(html); - doc.close(); - iframe.onload = () => { - setTimeout(() => { - iframe.contentWindow?.focus(); - iframe.contentWindow?.print(); - setTimeout(() => document.body.removeChild(iframe), 5000); - }, 600); - }; - } - } -} +/** + * SEO/GEO Audit Report Generator v2 + * Comprehensive standalone HTML reports with all audit sections + */ + +export interface SeoAuditData { + url: string; + domain: string; + protocol?: string; + responseTime: number; + server: string; + htmlSize: number; + title: string | null; + titleLength: number; + titleStatus: string; + metaDescription: string | null; + descLength: number; + descStatus: string; + metaKeywords: string | null; + viewport: string | null; + charset: string | null; + robotsDirectives: string | null; + canonical: string | null; + hasCanonicalMismatch: boolean; + xFrameOptions?: string; + h1Count: number; + h2Count: number; + h3Count: number; + h4Count: number; + headingStatus: string; + headings: { level: number; text: string }[]; + links?: { + total: number; + internal: number; + external: number; + nofollow: number; + sampleExternal?: { href: string; text: string; nofollow: boolean }[]; + }; + images?: { + total: number; + withAlt: number; + withoutAlt: number; + lazyLoaded: number; + altCoverage: number; + sampleWithoutAlt?: string[]; + }; + content?: { + wordCount: number; + sentenceCount: number; + paragraphCount: number; + avgWordsPerSentence: number; + textPreview?: string; + }; + openGraph?: { title: string | null; description: string | null; image: string | null; type: string | null; url?: string | null }; + twitterCard?: { card: string | null; title?: string | null; description?: string | null }; + hreflang?: string[]; + performance?: { + inlineStyles: number; + inlineScripts?: number; + externalScripts: number; + externalStylesheets: number; + hasPreconnect: boolean; + hasPreload: boolean; + hasDnsPrefetch: boolean; + usesAsyncScripts: boolean; + usesDeferScripts: boolean; + contentEncoding?: string; + }; + accessibility?: { hasLangAttr: boolean; hasAriaLabels: boolean; hasAltOnFirstImage: boolean }; + structuredData?: { hasJsonLd: boolean; hasMicrodata: boolean; types: { type: string; found: boolean }[] }; + scores?: { overall: number; technical: number; content: number; performance: number; social: number }; + issues?: { severity: string; category: string; message: string }[]; +} + +const sc = (s: number): string => + s >= 80 ? "#22c55e" : s >= 60 ? "#f59e0b" : "#ef4444"; + +const svc = (s: string): string => + s === "critical" ? "#ef4444" : s === "warning" ? "#f59e0b" : "#6b7280"; + +const badge = (label: string, color: string): string => + '' + label + ''; + +const passFail = (val: boolean | null | undefined, yesLabel?: string, noLabel?: string): string => + val ? badge(yesLabel || "PASS", "#22c55e") : badge(noLabel || "FAIL", "#ef4444"); + +const statusBadge = (status: string): string => { + if (status === "good" || status === "ok" || status === "pass") return badge("GOOD", "#22c55e"); + if (status === "missing" || status === "fail") return badge("MISSING", "#ef4444"); + if (status === "too_long" || status === "warning" || status === "multiple_h1") return badge("WARNING", "#f59e0b"); + if (status === "mismatch" || status === "missing_h1") return badge("CRITICAL", "#ef4444"); + return badge(status.toUpperCase(), "#6b7280"); +}; + +const t = (v: string | null | undefined, fallback: string): string => + v ? v : '' + fallback + ''; + +const fixMap: Record = { + Meta: "Ensure every page has a unique title tag (50-60 chars) with primary keyword near start. Write meta descriptions (150-160 chars) with a clear CTA. Add viewport meta tag for mobile compatibility. Include target keywords naturally without stuffing.", + Content: "Maintain exactly one H1 per page containing the primary keyword. Build logical heading hierarchy (H1 > H2 > H3). Aim for 1000+ words on core pages. Ensure each paragraph adds value. Use short paragraphs (3-4 sentences) for readability.", + Technical: "Set self-referencing canonical tags on all pages. Ensure valid SSL certificate with proper HTTPS redirect. Check robots.txt is not blocking important pages. Fix redirect chains (max 2 hops). Implement proper 301 redirects for moved pages.", + Mobile: "Add viewport meta tag. Test with Google Mobile-Friendly test. Ensure tap targets are minimum 48x48px. Avoid horizontal scroll. Use responsive images with srcset.", + Security: "Migrate to HTTPS with valid SSL certificate. Set up HTTP-to-HTTPS redirects. Configure HSTS headers. Set X-Frame-Options to prevent clickjacking. Implement Content-Security-Policy headers.", + Performance: "Minimize inline styles. Add preconnect hints for third-party domains. Implement lazy loading for images. Use async/defer for non-critical scripts. Compress images (WebP/AVIF). Enable Brotli/gzip compression. Minimize render-blocking resources.", + Social: "Add Open Graph tags (og:title, og:description, og:image, og:url, og:type). Implement Twitter Card meta tags. Ensure OG images are 1200x630px minimum. Validate with Facebook Sharing Debugger and Twitter Card Validator.", + Accessibility: "Add lang attribute to tag. Implement ARIA labels for interactive elements. Add descriptive alt text to all images. Ensure keyboard navigation works. Use sufficient color contrast ratios (WCAG AA minimum).", + Links: "Fix or remove broken internal links. Add external links to authoritative sources. Review nofollow attributes on external links. Ensure descriptive anchor text (avoid 'click here'). Implement logical internal linking structure.", + "Structured Data": "Implement JSON-LD structured data for relevant content types. Validate with Google Rich Results Test. Add FAQ, Article, Product, or Organization schema as appropriate. Use schema.org markup for entities and facts.", +}; + +export function generateSeoReportHtml(d: SeoAuditData): string { + const now = new Date().toLocaleString(); + + // --- Issue Rows --- + const criticalIssues = (d.issues || []).filter(i => i.severity === "critical"); + const warningIssues = (d.issues || []).filter(i => i.severity === "warning"); + const infoIssues = (d.issues || []).filter(i => i.severity === "info"); + + const issueRow = (issue: { severity: string; category: string; message: string }): string => + "" + badge(issue.severity.toUpperCase(), svc(issue.severity)) + "" + issue.category + "" + issue.message + "" + (fixMap[issue.category] || "Review and address this issue based on SEO best practices.") + ""; + + const allIssueRows = (d.issues || []).map(issueRow).join(""); + + // --- Heading Rows --- + const headingRows = (d.headings || []).slice(0, 30).map((h) => + "" + h.level + "" + h.text + "" + ).join(""); + + // --- Structured Data Rows --- + const sdTypes = d.structuredData?.types || []; + const sdRows = sdTypes.map((s) => + "" + s.type + "" + passFail(s.found, "FOUND", "NOT FOUND") + "" + ).join(""); + + // --- External Link Rows --- + const extLinks = d.links?.sampleExternal || []; + const extLinkRows = extLinks.slice(0, 15).map((l) => + "" + l.href + "" + (l.text || "N/A") + "" + (l.nofollow ? badge("NOFOLLOW", "#f59e0b") : badge("FOLLOW", "#22c55e")) + "" + ).join(""); + + // --- Missing Alt Image Rows --- + const missingAlts = d.images?.sampleWithoutAlt || []; + const altRows = missingAlts.slice(0, 10).map((src) => + "" + src + "" + ).join(""); + + // --- Hreflang Rows --- + const hreflangTags = d.hreflang || []; + const hreflangRows = hreflangTags.map((h) => + "" + h + "" + ).join(""); + + // --- GEO Score Calculation --- + const geoScore = calculateGeoScore(d); + const geoColor = sc(geoScore); + + // --- Action Plan --- + const actionPlan = generateActionPlan(d); + + // --- Build HTML --- + const css = "*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0f172a;color:#e2e8f0;line-height:1.6;padding:40px}@media print{body{background:#fff;color:#1e293b;padding:20px}}.container{max-width:960px;margin:0 auto}@media print{.container{max-width:100%}}.hero{text-align:center;padding:32px;background:linear-gradient(135deg,#1e293b 0%,#0f172a 100%);border-radius:20px;margin-bottom:32px;border:1px solid #334155}@media print{.hero{background:#f8fafc;border:1px solid #e2e8f0}}.hero h1{font-size:30px;margin-bottom:4px}.hero .url{color:#60a5fa;font-family:monospace;font-size:13px;word-break:break-all}.hero .meta{color:#94a3b8;margin-top:8px;font-size:12px}.scores{display:grid;grid-template-columns:repeat(6,1fr);gap:10px;margin:24px 0}.score-card{text-align:center;padding:18px 8px;background:#1e293b;border-radius:14px;border:1px solid #334155}@media print{.score-card{background:#f8fafc;border:1px solid #e2e8f0}}.score-value{font-size:34px;font-weight:800}.score-label{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;margin-top:4px}table{width:100%;border-collapse:collapse;margin:14px 0;background:#1e293b;border-radius:14px;overflow:hidden;border:1px solid #334155}@media print{table{background:#f8fafc;border:1px solid #e2e8f0}}th,td{padding:10px 14px;text-align:left;border-bottom:1px solid #334155;font-size:13px}@media print{th,td{border-bottom:1px solid #e2e8f0}}th{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;background:#162032}@media print{th{background:#f1f5f9;color:#64748b}}tr:hover{background:#1a2744}@media print{tr:hover{background:#f8fafc}}a{color:#60a5fa}.section{background:#1e293b;border-radius:16px;padding:24px;margin-bottom:20px;border:1px solid #334155}@media print{.section{background:#f8fafc;border:1px solid #e2e8f0}}h2{font-size:18px;margin-bottom:14px;padding-bottom:8px;border-bottom:2px solid #334155;display:flex;align-items:center;gap:8px}h2 .icon{font-size:20px}.stat-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin:12px 0}.stat-item{background:#162032;padding:12px;border-radius:10px;text-align:center}@media print{.stat-item{background:#f1f5f9}}.stat-value{font-size:22px;font-weight:800}.stat-label{font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-top:2px}.action-item{display:flex;gap:12px;padding:10px 0;border-bottom:1px solid #334155}.action-item:last-child{border:none}.action-priority{font-size:10px;font-weight:800;padding:2px 8px;border-radius:4px;text-transform:uppercase;white-space:nowrap}.action-priority.high{background:#ef444420;color:#ef4444}.action-priority.medium{background:#f59e0b20;color:#f59e0b}.action-priority.low{background:#22c55e20;color:#22c55e}.geo-bar{height:8px;background:#334155;border-radius:4px;overflow:hidden;margin:8px 0}.geo-fill{height:100%;border-radius:4px;transition:width 0.3s}.footer{text-align:center;padding:32px;color:#64748b;font-size:12px}"; + + let html = "SEO/GEO Audit - " + + (d.domain || "report") + + "
"; + + // === HERO === + html += "

Comprehensive SEO/GEO Audit Report

" + + "

" + (d.url || "N/A") + "

" + + "

Generated by PromptArch Vibe Architect | " + now + "

"; + + // === EXECUTIVE SUMMARY === + html += "

01 Executive Summary

" + + "
" + + "
" + (d.scores?.overall || 0) + "/100
Overall Score
" + + "
" + (d.issues?.length || 0) + "
Issues Found
" + + "
" + criticalIssues.length + "
Critical Issues
" + + "
" + geoScore + "/100
GEO Readiness
" + + "
"; + + // === SCORES === + html += "

02 Scoring Breakdown

" + + "
" + + scoreCard("Overall", d.scores?.overall || 0) + + scoreCard("Technical", d.scores?.technical || 0) + + scoreCard("Content", d.scores?.content || 0) + + scoreCard("Performance", d.scores?.performance || 0) + + scoreCard("Social", d.scores?.social || 0) + + "
" + geoScore + "
GEO
" + + "
" + + geoBar("Technical SEO", d.scores?.technical || 0) + + geoBar("Content Quality", d.scores?.content || 0) + + geoBar("Performance", d.scores?.performance || 0) + + geoBar("Social/OG", d.scores?.social || 0) + + geoBar("GEO Readiness", geoScore) + + "
"; + + // === META TAGS === + html += "

03 Meta Tags Analysis

" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
ElementValueStatus
Title" + t(d.title, "MISSING") + " (" + (d.titleLength || 0) + " chars)" + statusBadge(d.titleStatus) + "
Meta Description" + t(d.metaDescription, "MISSING") + " (" + (d.descLength || 0) + " chars)" + statusBadge(d.descStatus) + "
Meta Keywords" + t(d.metaKeywords, "Not set (modern SEO does not require)") + "Not a ranking factor
Viewport" + t(d.viewport, "MISSING") + "" + passFail(!!d.viewport) + "
Charset" + t(d.charset, "MISSING") + "" + passFail(!!d.charset) + "
Canonical" + (d.canonical || "None") + "" + (d.hasCanonicalMismatch ? badge("MISMATCH", "#ef4444") : d.canonical ? badge("OK", "#22c55e") : badge("MISSING", "#ef4444")) + "
Robots" + (d.robotsDirectives || "None") + "-
X-Frame-Options" + t(d.xFrameOptions, "Not set") + "" + passFail(!!d.xFrameOptions) + "
Protocol" + (d.protocol || "Unknown") + "" + (d.protocol === "HTTPS" ? badge("SECURE", "#22c55e") : badge("INSECURE", "#ef4444")) + "
"; + + // === SOCIAL / OPEN GRAPH === + html += "

04 Social & Open Graph

" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
PropertyValueStatus
OG Title" + t(d.openGraph?.title, "Missing") + "" + passFail(!!d.openGraph?.title) + "
OG Description" + t(d.openGraph?.description, "Missing") + "" + passFail(!!d.openGraph?.description) + "
OG Image" + (d.openGraph?.image ? '' + d.openGraph.image.substring(0, 80) + '...' : "Missing") + "" + passFail(!!d.openGraph?.image) + "
OG Type" + t(d.openGraph?.type, "Missing") + "" + passFail(!!d.openGraph?.type) + "
OG URL" + t(d.openGraph?.url, "Missing") + "" + passFail(!!d.openGraph?.url) + "
Twitter Card" + t(d.twitterCard?.card, "Not set") + "" + passFail(!!d.twitterCard?.card) + "
Twitter Title" + t(d.twitterCard?.title, "Missing") + "" + passFail(!!d.twitterCard?.title) + "
Twitter Description" + t(d.twitterCard?.description, "Missing") + "" + passFail(!!d.twitterCard?.description) + "
"; + + // === HEADINGS === + html += "

05 Heading Structure

" + + "
" + + "
" + d.h1Count + "
H1
" + + "
" + d.h2Count + "
H2
" + + "
" + d.h3Count + "
H3
" + + "
" + d.h4Count + "
H4+
" + + "
" + + "

Status: " + statusBadge(d.headingStatus) + "

" + + (headingRows ? "

Heading hierarchy (up to 30 shown):

" + headingRows + "
LevelText
" : "

No headings found.

") + + "
"; + + // === LINKS === + html += "

06 Links Analysis

" + + "
" + + "
" + (d.links?.total || 0) + "
Total Links
" + + "
" + (d.links?.internal || 0) + "
Internal
" + + "
" + (d.links?.external || 0) + "
External
" + + "
" + (d.links?.nofollow || 0) + "
Nofollow
" + + "
"; + if (extLinkRows) { + html += "

Sample external links:

" + + "" + extLinkRows + "
URLAnchor TextRel
"; + } + html += "
"; + + // === IMAGES === + html += "

07 Images & Alt Text

" + + "
" + + "
" + (d.images?.total || 0) + "
Total Images
" + + "
" + (d.images?.withAlt || 0) + "
With Alt
" + + "
" + (d.images?.withoutAlt || 0) + "
Missing Alt
" + + "
" + (d.images?.altCoverage || 0) + "%
Alt Coverage
" + + "
" + + "

Lazy Loaded: " + passFail((d.images?.lazyLoaded || 0) > 0) + " (" + (d.images?.lazyLoaded || 0) + " images)

"; + if (altRows) { + html += "

Images missing alt text:

" + + "" + altRows + "
Image Source
"; + } + html += "
"; + + // === CONTENT === + html += "

08 Content Analysis

" + + "
" + + "
" + (d.content?.wordCount || 0) + "
Word Count
" + + "
" + (d.content?.sentenceCount || 0) + "
Sentences
" + + "
" + (d.content?.paragraphCount || 0) + "
Paragraphs
" + + "
" + (d.content?.avgWordsPerSentence || 0) + "
Avg Words/Sentence
" + + "
" + + "" + + "" + + "" + + "
MetricValueRecommendation
Word Count" + (d.content?.wordCount || 0) + "" + ((d.content?.wordCount || 0) >= 1000 ? badge("GOOD", "#22c55e") : badge("BELOW 1000", "#f59e0b")) + " Aim for 1000+ on core pages
Readability" + (d.content?.avgWordsPerSentence || 0) + " avg words/sentence" + ((d.content?.avgWordsPerSentence || 0) <= 20 ? badge("GOOD", "#22c55e") : badge("LONG", "#f59e0b")) + " Keep under 20 for readability
"; + + // === PERFORMANCE === + html += "

09 Performance Signals

" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
MetricValueStatus
Server" + (d.server || "Unknown") + "-
Response Time" + d.responseTime + "ms" + (d.responseTime < 500 ? badge("FAST", "#22c55e") : d.responseTime < 1500 ? badge("OK", "#f59e0b") : badge("SLOW", "#ef4444")) + "
HTML Size" + (d.htmlSize || 0).toLocaleString() + " bytes-
Content Encoding" + t(d.performance?.contentEncoding, "None detected") + "" + passFail(!!d.performance?.contentEncoding) + "
External Scripts" + (d.performance?.externalScripts || 0) + "" + ((d.performance?.externalScripts || 0) <= 5 ? badge("OK", "#22c55e") : badge("HIGH", "#f59e0b")) + "
External Stylesheets" + (d.performance?.externalStylesheets || 0) + "-
Inline Styles" + (d.performance?.inlineStyles || 0) + "" + ((d.performance?.inlineStyles || 0) <= 10 ? badge("OK", "#22c55e") : badge("HIGH", "#f59e0b")) + "
Inline Scripts" + (d.performance?.inlineScripts || 0) + "" + ((d.performance?.inlineScripts || 0) === 0 ? badge("GOOD", "#22c55e") : badge("FOUND", "#f59e0b")) + "
Preconnect" + (d.performance?.hasPreconnect ? "Yes" : "No") + "" + passFail(d.performance?.hasPreconnect) + "
Preload" + (d.performance?.hasPreload ? "Yes" : "No") + "" + passFail(d.performance?.hasPreload) + "
DNS Prefetch" + (d.performance?.hasDnsPrefetch ? "Yes" : "No") + "" + passFail(d.performance?.hasDnsPrefetch) + "
Async/Defer Scripts" + (d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts ? "Yes" : "No") + "" + passFail(d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts) + "
"; + + // === ACCESSIBILITY === + html += "

10 Accessibility

" + + "" + + "" + + "" + + "" + + "" + + "
CheckStatusImpact
HTML lang attribute" + passFail(d.accessibility?.hasLangAttr) + "Screen readers, SEO
ARIA labels present" + passFail(d.accessibility?.hasAriaLabels) + "Assistive technology
First image has alt text" + passFail(d.accessibility?.hasAltOnFirstImage) + "Screen readers
Alt text coverage" + (d.images?.altCoverage || 0) + "%" + ((d.images?.altCoverage || 0) >= 90 ? badge("GOOD", "#22c55e") : (d.images?.altCoverage || 0) >= 70 ? badge("FAIR", "#f59e0b") : badge("POOR", "#ef4444")) + "
"; + + // === STRUCTURED DATA === + html += "

11 Structured Data / Schema

" + + "" + + "" + + "" + + "
FormatStatus
JSON-LD" + passFail(d.structuredData?.hasJsonLd) + "
Microdata" + passFail(d.structuredData?.hasMicrodata) + "
"; + if (sdRows) { + html += "

Detected schema types:

" + + "" + sdRows + "
Schema TypeFound
"; + } else if (!d.structuredData?.hasJsonLd && !d.structuredData?.hasMicrodata) { + html += "

No structured data detected. Adding JSON-LD schema (FAQ, Article, Organization, Product) can significantly improve search visibility and enable rich results.

"; + } + html += "
"; + + // === HREFLANG === + if (hreflangTags.length > 0) { + html += "

12 Hreflang Tags

" + + "" + hreflangRows + "
Language-Region
"; + } + + // === GEO ANALYSIS === + html += "

13 GEO (Generative Engine Optimization)

" + + "
" + geoScore + "/100
GEO Readiness Score
" + + "
" + (d.structuredData?.hasJsonLd ? "Yes" : "No") + "
Schema Markup
" + + geoBar("Factual Content Structure", geoFactualScore(d)) + + geoBar("Entity Schema", geoEntityScore(d)) + + geoBar("Content Depth", geoContentDepthScore(d)) + + geoBar("Citeability", geoCiteabilityScore(d)) + + "

What is GEO?

" + + "

GEO (Generative Engine Optimization) is the practice of optimizing content for AI-powered search engines like ChatGPT, Claude, Perplexity, and Google AI Overviews. Unlike traditional SEO which targets algorithmic rankings, GEO focuses on making your content authoritative, factual, and well-structured enough for AI models to cite as sources.

" + + "

GEO Improvement Checklist

" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
FactorStatusAction
Structured Data / Schema" + passFail(d.structuredData?.hasJsonLd) + "Add JSON-LD for Organization, FAQ, Article, Product schemas
Heading Hierarchy" + statusBadge(d.headingStatus) + "One H1, logical H2-H4 hierarchy with keyword-rich headings
Content Depth (1000+ words)" + passFail((d.content?.wordCount || 0) >= 1000) + "Expand core pages with comprehensive, authoritative content
FAQ / Q&A Format" + passFail(d.structuredData?.types?.some((s) => s.type === "FAQ") && d.structuredData?.types?.some((s) => s.found)) + "Add FAQ schema with common questions about your topic
Clear Facts & Statistics" + passFail((d.content?.wordCount || 0) >= 500) + "Include verifiable data points, statistics, and citations
Lists & Tables" + passFail(d.h2Count >= 3) + "Use structured lists and comparison tables for scannability
Authoritative Tone" + passFail(d.h1Count === 1) + "Write expert-level content with clear author attribution
Meta Description CTA" + passFail(!!d.metaDescription && d.metaDescription.length >= 120) + "Include clear call-to-action in meta descriptions
"; + + // === ISSUES === + html += "

14 Issues & How to Fix (" + (d.issues?.length || 0) + " found)

" + + "
" + + "" + badge("CRITICAL: " + criticalIssues.length, "#ef4444") + "" + + "" + badge("WARNING: " + warningIssues.length, "#f59e0b") + "" + + "" + badge("INFO: " + infoIssues.length, "#6b7280") + "" + + "
" + + "" + + (allIssueRows || "") + + "
SeverityCategoryIssueHow to Fix
No issues detected. Great job!
"; + + // === ACTION PLAN === + html += "

15 Prioritized Action Plan

" + + "

Recommended fixes ordered by impact and priority.

" + + actionPlan + "
"; + + // === FAQ RECOMMENDATIONS === + html += "

16 Recommended FAQ Schema Questions

" + + "

Based on your content analysis, consider adding these FAQ items to improve featured snippet eligibility and GEO performance.

" + + generateFaqRecommendations(d) + "
"; + + // === GENERAL RECOMMENDATIONS === + html += "

17 SEO/GEO Best Practices Checklist

    " + + "
  1. Title Tags: Keep under 60 characters. Place primary keyword near the beginning. Make each page title unique.
  2. " + + "
  3. Meta Descriptions: Write 150-160 characters with a clear call-to-action. Include target keywords naturally.
  4. " + + "
  5. Heading Structure: Use exactly one H1 per page. Create a logical hierarchy. Include keywords in headings.
  6. " + + "
  7. Content Quality: Aim for 1000+ words on core pages. Write for users first, search engines second. Update content regularly.
  8. " + + "
  9. Image Optimization: Add descriptive alt text to every image. Use WebP/AVIF format. Implement lazy loading. Compress file sizes.
  10. " + + "
  11. Page Speed: Minimize render-blocking resources. Enable compression (Brotli/gzip). Use CDN. Optimize images and fonts.
  12. " + + "
  13. Mobile Experience: Ensure responsive design. Test tap targets. Avoid horizontal scrolling. Optimize font sizes.
  14. " + + "
  15. Structured Data: Implement JSON-LD schema markup. Validate with Google Rich Results Test. Add FAQ, Article, or Product schema.
  16. " + + "
  17. Internal Linking: Create logical site architecture. Use descriptive anchor text. Ensure important pages are within 3 clicks of homepage.
  18. " + + "
  19. Technical SEO: Submit XML sitemap. Configure robots.txt properly. Fix broken links. Implement canonical tags. Monitor crawl errors.
  20. " + + "
  21. GEO Optimization: Structure content with clear facts, lists, and tables. Use schema markup for entities. Optimize for featured snippets. Create comprehensive, authoritative content that AI models can cite as sources.
  22. " + + "
  23. Security: Enforce HTTPS. Set HSTS headers. Configure X-Frame-Options. Implement Content-Security-Policy.
  24. " + + "
"; + + // === FOOTER === + html += "

PromptArch Vibe Architect | " + now + "

" + + "

rommark.dev/tools/promptarch

"; + + html += "
"; + return html; +} + +// --- Helper functions --- + +function scoreCard(label: string, value: number): string { + return "
" + value + "
" + label + "
"; +} + +function geoBar(label: string, value: number): string { + return "
" + label + "" + value + "%
" + + "
"; +} + +function calculateGeoScore(d: SeoAuditData): number { + let score = 0; + if (d.structuredData?.hasJsonLd) score += 20; + if (d.structuredData?.hasMicrodata) score += 5; + if (d.structuredData?.types?.some((s) => s.type === "FAQ" && s.found)) score += 15; + if (d.structuredData?.types?.some((s) => s.type === "Organization" && s.found)) score += 10; + if (d.structuredData?.types?.some((s) => s.type === "Article" && s.found)) score += 5; + if (d.h1Count === 1) score += 10; + if (d.h2Count >= 3) score += 5; + if ((d.content?.wordCount || 0) >= 1000) score += 15; + else if ((d.content?.wordCount || 0) >= 500) score += 8; + if (d.openGraph?.title && d.openGraph?.description) score += 5; + if (d.metaDescription && d.metaDescription.length >= 120) score += 5; + if (d.accessibility?.hasLangAttr) score += 5; + return Math.min(score, 100); +} + +function geoFactualScore(d: SeoAuditData): number { + let s = 0; + if ((d.content?.wordCount || 0) >= 1000) s += 40; + else if ((d.content?.wordCount || 0) >= 500) s += 25; + else if ((d.content?.wordCount || 0) >= 200) s += 10; + if (d.h2Count >= 3) s += 30; + if ((d.content?.paragraphCount || 0) >= 5) s += 30; + return Math.min(s, 100); +} + +function geoEntityScore(d: SeoAuditData): number { + if (d.structuredData?.hasJsonLd) return 80; + if (d.structuredData?.hasMicrodata) return 50; + return 10; +} + +function geoContentDepthScore(d: SeoAuditData): number { + let s = 0; + const wc = d.content?.wordCount || 0; + if (wc >= 2000) s += 40; + else if (wc >= 1000) s += 30; + else if (wc >= 500) s += 15; + if (d.h3Count >= 3) s += 20; + if (d.h4Count >= 2) s += 10; + if ((d.content?.paragraphCount || 0) >= 8) s += 15; + if ((d.links?.external || 0) >= 3) s += 15; + return Math.min(s, 100); +} + +function geoCiteabilityScore(d: SeoAuditData): number { + let s = 0; + if (d.title && d.title.length >= 30) s += 15; + if (d.metaDescription && d.metaDescription.length >= 120) s += 15; + if (d.openGraph?.title) s += 10; + if (d.structuredData?.types?.some((t) => t.type === "Article" && t.found)) s += 20; + if (d.accessibility?.hasLangAttr) s += 10; + if (d.h1Count === 1) s += 10; + if (d.canonical) s += 10; + if ((d.content?.wordCount || 0) >= 1000) s += 10; + return Math.min(s, 100); +} + +function generateActionPlan(d: SeoAuditData): string { + const items: { priority: string; action: string; impact: string }[] = []; + + // Critical issues first + (d.issues || []).filter(i => i.severity === "critical").forEach(issue => { + items.push({ priority: "high", action: issue.message, impact: issue.category }); + }); + + // Data-driven actions + if (!d.title) items.push({ priority: "high", action: "Add a unique title tag (50-60 chars) with primary keyword", impact: "Meta / SEO" }); + if (!d.metaDescription) items.push({ priority: "high", action: "Add meta description (150-160 chars) with target keyword and CTA", impact: "Meta / CTR" }); + if (!d.canonical) items.push({ priority: "high", action: "Add self-referencing canonical tag to prevent duplicate content issues", impact: "Technical" }); + if (!d.viewport) items.push({ priority: "high", action: "Add viewport meta tag for proper mobile rendering", impact: "Mobile" }); + if (d.protocol !== "HTTPS") items.push({ priority: "high", action: "Migrate to HTTPS with valid SSL certificate and proper redirects", impact: "Security" }); + if (d.h1Count === 0) items.push({ priority: "high", action: "Add a single H1 heading with the primary keyword", impact: "Content" }); + if (d.h1Count > 1) items.push({ priority: "medium", action: "Consolidate to exactly one H1 heading per page", impact: "Content" }); + if (!d.structuredData?.hasJsonLd) items.push({ priority: "medium", action: "Implement JSON-LD structured data (FAQ, Organization, Article schemas)", impact: "Structured Data / GEO" }); + if (!d.openGraph?.title) items.push({ priority: "medium", action: "Add Open Graph tags (og:title, og:description, og:image) for social sharing", impact: "Social" }); + if (!d.twitterCard?.card) items.push({ priority: "medium", action: "Add Twitter Card meta tags for better Twitter previews", impact: "Social" }); + if (!d.accessibility?.hasLangAttr) items.push({ priority: "medium", action: 'Add lang attribute to tag (e.g. lang="en")', impact: "Accessibility" }); + if ((d.images?.altCoverage || 0) < 90) items.push({ priority: "medium", action: "Add descriptive alt text to all images (currently " + (d.images?.altCoverage || 0) + "% coverage)", impact: "Accessibility / SEO" }); + if (!d.performance?.hasPreconnect) items.push({ priority: "medium", action: "Add preconnect hints for third-party domains to improve load time", impact: "Performance" }); + if (!d.performance?.usesAsyncScripts && !d.performance?.usesDeferScripts) items.push({ priority: "medium", action: "Use async or defer attributes on non-critical scripts", impact: "Performance" }); + if ((d.images?.lazyLoaded || 0) === 0 && (d.images?.total || 0) > 0) items.push({ priority: "medium", action: "Implement lazy loading for images to improve initial page load", impact: "Performance" }); + if (!d.structuredData?.types?.some((t) => t.type === "FAQ" && t.found)) items.push({ priority: "low", action: "Add FAQ schema with common questions about your topic for featured snippet eligibility", impact: "GEO" }); + if ((d.content?.wordCount || 0) < 1000) items.push({ priority: "low", action: "Expand content to 1000+ words on core pages for better depth and authority", impact: "Content / GEO" }); + if ((d.images?.total || 0) > 0 && !d.images?.lazyLoaded) items.push({ priority: "low", action: "Add loading=\"lazy\" to below-the-fold images", impact: "Performance" }); + + // Warning issues + (d.issues || []).filter(i => i.severity === "warning").forEach(issue => { + if (!items.some(it => it.action === issue.message)) { + items.push({ priority: "medium", action: issue.message, impact: issue.category }); + } + }); + + // Deduplicate and limit + const seen = new Set(); + const unique = items.filter(item => { + if (seen.has(item.action)) return false; + seen.add(item.action); + return true; + }).slice(0, 20); + + return unique.map(item => + "
" + item.priority + "
" + + "
" + item.action + "
" + + "
Impact: " + item.impact + "
" + ).join(""); +} + +function generateFaqRecommendations(d: SeoAuditData): string { + const domain = d.domain || "this site"; + const title = d.title || domain; + const questions: string[] = []; + + questions.push("What is " + title + "?
Provide a clear, concise answer (40-60 words) explaining what " + domain + " offers and its primary value proposition."); + questions.push("How does " + domain + " work?
Explain the core functionality, process, or service in simple terms."); + questions.push("What are the main benefits of using " + domain + "?
List 3-5 key benefits with brief explanations."); + questions.push("Is " + domain + " free or paid?
Provide clear pricing information or state if the service is free."); + questions.push("How do I get started with " + domain + "?
Outline the steps a new user should take to begin."); + + if (d.protocol === "HTTPS") { + questions.push("Is " + domain + " secure?
Confirm security measures in place (SSL, data protection, etc.)."); + } + if ((d.content?.wordCount || 0) >= 300) { + questions.push("What makes " + domain + " different from competitors?
Highlight unique selling points and differentiators."); + } + + return questions.map((q, i) => { + const parts = q.split("
"); + return "
" + (i + 1) + ". " + parts[0] + "
" + + "
" + parts[1] + "
"; + }).join(""); +} + +export function downloadSeoReport(d: SeoAuditData, format: "html" | "pdf") { + const html = generateSeoReportHtml(d); + + if (format === "html") { + const blob = new Blob([html], { type: "text/html" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "seo-geo-audit-" + (d.domain || "report") + "-" + new Date().toISOString().slice(0, 10) + ".html"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } else { + const iframe = document.createElement("iframe"); + iframe.style.position = "fixed"; + iframe.style.right = "0"; + iframe.style.bottom = "0"; + iframe.style.width = "0"; + iframe.style.height = "0"; + iframe.style.border = "none"; + document.body.appendChild(iframe); + const doc = iframe.contentDocument || iframe.contentWindow?.document; + if (doc) { + doc.open(); + doc.write(html); + doc.close(); + iframe.onload = () => { + setTimeout(() => { + iframe.contentWindow?.focus(); + iframe.contentWindow?.print(); + setTimeout(() => document.body.removeChild(iframe), 5000); + }, 600); + }; + } + } +}