feat: comprehensive 17-section SEO/GEO audit report with GEO scoring (v2.1.0)

This commit is contained in:
admin
2026-03-18 22:06:50 +00:00
Unverified
parent 70671f47e1
commit a7958d2e63
3 changed files with 604 additions and 266 deletions

View File

@@ -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

View File

@@ -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 |

View File

@@ -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<string, string> = {
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 <html>. 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) =>
"<tr><td><span style=\"display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;color:white;background:" +
svc(issue.severity) +
"\">" +
issue.severity.toUpperCase() +
"</span></td><td style=\"font-weight:600\">" +
issue.category +
"</td><td>" +
issue.message +
"</td><td style=\"white-space:pre-wrap;font-size:12px;color:#22c55e\">" +
(fixInstructions[issue.category] || "Review and address this issue.") +
"</td></tr>"
)
.join("");
const headingRows = (d.headings || [])
.map((h) => "<tr><td style=\"font-weight:700\">H" + h.level + "</td><td>" + h.text + "</td></tr>")
.join("");
const t = (v: string | null | undefined, fallback = "None") => v || "<em>" + fallback + "</em>";
return "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>SEO Audit - " +
(d.domain || "report") +
"</title><style>*{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}h1{font-size:28px;margin-bottom:8px}h2{font-size:20px;margin:32px 0 16px;padding-bottom:8px;border-bottom:2px solid #334155}.container{max-width:900px;margin:0 auto}.meta{text-align:center;padding:24px;background:#1e293b;border-radius:16px;margin-bottom:32px}.scores{display:grid;grid-template-columns:repeat(5,1fr);gap:12px;margin:24px 0}.score-card{text-align:center;padding:20px 12px;background:#1e293b;border-radius:12px}.score-value{font-size:36px;font-weight:800}.score-label{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#94a3b8;margin-top:4px}table{width:100%;border-collapse:collapse;margin:16px 0;background:#1e293b;border-radius:12px;overflow:hidden}th,td{padding:12px 16px;text-align:left;border-bottom:1px solid #334155}th{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#94a3b8}tr:hover{background:#334155}a{color:#60a5fa}.section{background:#1e293b;border-radius:16px;padding:24px;margin-bottom:20px}@media print{body{background:white;color:#1e293b}.section,.meta,.scores{background:#f8fafc;border:1px solid #e2e8f0}.score-value{color:#0f172a!important}}</style></head><body><div class=\"container\"><div class=\"meta\"><h1>SEO/GEO Audit Report</h1><p style=\"color:#60a5fa;font-family:monospace\">" +
(d.url || "N/A") +
"</p><p style=\"color:#94a3b8;margin-top:8px\">PromptArch Vibe Architect | " +
now +
"</p></div><div class=\"scores\"><div class=\"score-card\"><div class=\"score-value\" style=\"color:" +
sc(d.scores?.overall || 0) +
"\">" +
(d.scores?.overall || 0) +
"</div><div class=\"score-label\">Overall</div></div><div class=\"score-card\"><div class=\"score-value\" style=\"color:" +
sc(d.scores?.technical || 0) +
"\">" +
(d.scores?.technical || 0) +
"</div><div class=\"score-label\">Technical</div></div><div class=\"score-card\"><div class=\"score-value\" style=\"color:" +
sc(d.scores?.content || 0) +
"\">" +
(d.scores?.content || 0) +
"</div><div class=\"score-label\">Content</div></div><div class=\"score-card\"><div class=\"score-value\" style=\"color:" +
sc(d.scores?.performance || 0) +
"\">" +
(d.scores?.performance || 0) +
"</div><div class=\"score-label\">Performance</div></div><div class=\"score-card\"><div class=\"score-value\" style=\"color:" +
sc(d.scores?.social || 0) +
"\">" +
(d.scores?.social || 0) +
"</div><div class=\"score-label\">Social</div></div></div>" +
// Meta Tags
"<div class=\"section\"><h2>Meta Tags</h2><table><tr><th>Element</th><th>Value</th><th>Status</th></tr><tr><td>Title</td><td>" +
t(d.title, "MISSING") +
" (" +
(d.titleLength || 0) +
" chars)</td><td>" +
(d.titleStatus || "?") +
"</td></tr><tr><td>Description</td><td>" +
t(d.metaDescription, "MISSING") +
" (" +
(d.descLength || 0) +
" chars)</td><td>" +
(d.descStatus || "?") +
"</td></tr><tr><td>Viewport</td><td>" +
t(d.viewport, "MISSING") +
"</td><td>" +
(d.viewport ? "OK" : "Missing") +
"</td></tr><tr><td>Canonical</td><td>" +
(d.canonical || "None") +
"</td><td>" +
(d.hasCanonicalMismatch ? "MISMATCH" : d.canonical ? "OK" : "Missing") +
"</td></tr><tr><td>Robots</td><td>" +
(d.robotsDirectives || "None") +
"</td><td></td></tr></table></div>" +
// Social
"<div class=\"section\"><h2>Social / Open Graph</h2><table><tr><th>Property</th><th>Value</th></tr><tr><td>OG Title</td><td>" +
(d.openGraph?.title || "None") +
"</td></tr><tr><td>OG Description</td><td>" +
(d.openGraph?.description || "None") +
"</td></tr><tr><td>OG Image</td><td>" +
(d.openGraph?.image || "None") +
"</td></tr><tr><td>OG Type</td><td>" +
(d.openGraph?.type || "None") +
"</td></tr><tr><td>Twitter Card</td><td>" +
(d.twitterCard?.card || "None") +
"</td></tr></table></div>" +
// Headings
"<div class=\"section\"><h2>Headings</h2><p style=\"color:#94a3b8\">H1: " +
d.h1Count +
" | H2: " +
d.h2Count +
" | H3: " +
d.h3Count +
" | H4: " +
d.h4Count +
" [" +
(d.headingStatus || "?") +
"]</p>" +
(headingRows ? "<table><tr><th>Level</th><th>Text</th></tr>" + headingRows + "</table>" : "") +
"</div>" +
// Links & Images
"<div class=\"section\"><h2>Links & Images</h2><table><tr><th>Metric</th><th>Value</th></tr><tr><td>Total Links</td><td>" +
(d.links?.total || 0) +
"</td></tr><tr><td>Internal</td><td>" +
(d.links?.internal || 0) +
"</td></tr><tr><td>External</td><td>" +
(d.links?.external || 0) +
"</td></tr><tr><td>Nofollow</td><td>" +
(d.links?.nofollow || 0) +
"</td></tr><tr><td>Images</td><td>" +
(d.images?.total || 0) +
" (" +
(d.images?.altCoverage || 0) +
"% alt)</td></tr><tr><td>Missing Alt</td><td>" +
(d.images?.withoutAlt || 0) +
"</td></tr><tr><td>Lazy Loaded</td><td>" +
(d.images?.lazyLoaded || 0) +
"</td></tr></table></div>" +
// Content
"<div class=\"section\"><h2>Content</h2><table><tr><th>Metric</th><th>Value</th></tr><tr><td>Words</td><td>" +
(d.content?.wordCount || 0) +
"</td></tr><tr><td>Sentences</td><td>" +
(d.content?.sentenceCount || 0) +
"</td></tr><tr><td>Avg Words/Sentence</td><td>" +
(d.content?.avgWordsPerSentence || 0) +
"</td></tr></table></div>" +
// Performance
"<div class=\"section\"><h2>Performance</h2><table><tr><th>Metric</th><th>Value</th></tr><tr><td>Server</td><td>" +
(d.server || "Unknown") +
"</td></tr><tr><td>Response Time</td><td>" +
d.responseTime +
"ms</td></tr><tr><td>HTML Size</td><td>" +
(d.htmlSize || 0).toLocaleString() +
" bytes</td></tr><tr><td>External Scripts</td><td>" +
(d.performance?.externalScripts || 0) +
"</td></tr><tr><td>Inline Styles</td><td>" +
(d.performance?.inlineStyles || 0) +
"</td></tr><tr><td>Preconnect</td><td>" +
(d.performance?.hasPreconnect ? "Yes" : "No") +
"</td></tr><tr><td>Async/Defer</td><td>" +
(d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts ? "Yes" : "No") +
"</td></tr></table></div>" +
// Issues
"<div class=\"section\"><h2>Issues & How to Fix</h2><p style=\"color:#94a3b8\">" +
(d.issues?.length || 0) +
" issues found</p><table><tr><th>Severity</th><th>Category</th><th>Issue</th><th>How to Fix</th></tr>" +
(issueRows || "<tr><td colspan=\"4\">No issues.</td></tr>") +
"</table></div>" +
// Recommendations
"<div class=\"section\"><h2>SEO/GEO Recommendations</h2><ol style=\"padding-left:20px\"><li><strong>Title Tags:</strong> Keep under 60 chars, primary keyword near start, unique per page.</li><li><strong>Meta Descriptions:</strong> 150-160 chars with CTA, include target keywords naturally.</li><li><strong>Headings:</strong> One H1 per page, logical hierarchy, keywords in headings.</li><li><strong>Content:</strong> 1000+ words on core pages, authoritative, regularly updated.</li><li><strong>Images:</strong> Descriptive alt text, WebP format, lazy loading, compressed.</li><li><strong>Page Speed:</strong> Minimize render-blocking resources, enable compression, use CDN.</li><li><strong>Mobile:</strong> Responsive design, 48x48px tap targets, no horizontal scroll.</li><li><strong>Structured Data:</strong> JSON-LD schema, validate with Google Rich Results Test.</li><li><strong>Internal Linking:</strong> Logical architecture, descriptive anchors, 3-click rule.</li><li><strong>Technical:</strong> XML sitemap, robots.txt, fix broken links, canonical tags.</li><li><strong>GEO:</strong> Structure content with clear facts/lists/tables. Use schema for entities. Optimize for featured snippets. Create authoritative content AI models can cite.</li></ol></div><div style=\"text-align:center;padding:32px;color:#64748b;font-size:12px\"><p>PromptArch Vibe Architect | " +
now +
"</p><p><a href=\"https://rommark.dev/tools/promptarch/\">rommark.dev/tools/promptarch</a></p></div></div></body></html>";
}
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 =>
'<span style="display:inline-block;padding:2px 10px;border-radius:6px;font-size:11px;font-weight:700;color:white;background:' + color + '">' + label + '</span>';
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 : '<em style="color:#64748b">' + fallback + '</em>';
const fixMap: Record<string, string> = {
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 <html> 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 =>
"<tr><td>" + badge(issue.severity.toUpperCase(), svc(issue.severity)) + "</td><td style=\"font-weight:600\">" + issue.category + "</td><td>" + issue.message + "</td><td style=\"white-space:pre-wrap;font-size:12px;color:#22c55e\">" + (fixMap[issue.category] || "Review and address this issue based on SEO best practices.") + "</td></tr>";
const allIssueRows = (d.issues || []).map(issueRow).join("");
// --- Heading Rows ---
const headingRows = (d.headings || []).slice(0, 30).map((h) =>
"<tr><td style=\"font-weight:700;padding:4px 12px\"><span style=\"display:inline-block;padding:1px 8px;border-radius:4px;font-size:10px;background:#334155\">" + h.level + "</span></td><td>" + h.text + "</td></tr>"
).join("");
// --- Structured Data Rows ---
const sdTypes = d.structuredData?.types || [];
const sdRows = sdTypes.map((s) =>
"<tr><td>" + s.type + "</td><td>" + passFail(s.found, "FOUND", "NOT FOUND") + "</td></tr>"
).join("");
// --- External Link Rows ---
const extLinks = d.links?.sampleExternal || [];
const extLinkRows = extLinks.slice(0, 15).map((l) =>
"<tr><td style=\"max-width:300px;word-break:break-all\"><a href=\"" + l.href + "\" target=\"_blank\">" + l.href + "</a></td><td>" + (l.text || "N/A") + "</td><td>" + (l.nofollow ? badge("NOFOLLOW", "#f59e0b") : badge("FOLLOW", "#22c55e")) + "</td></tr>"
).join("");
// --- Missing Alt Image Rows ---
const missingAlts = d.images?.sampleWithoutAlt || [];
const altRows = missingAlts.slice(0, 10).map((src) =>
"<tr><td style=\"max-width:500px;word-break:break-all;font-size:11px\">" + src + "</td></tr>"
).join("");
// --- Hreflang Rows ---
const hreflangTags = d.hreflang || [];
const hreflangRows = hreflangTags.map((h) =>
"<tr><td>" + h + "</td></tr>"
).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 = "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>SEO/GEO Audit - " +
(d.domain || "report") +
"</title><style>" + css + "</style></head><body><div class=\"container\">";
// === HERO ===
html += "<div class=\"hero\"><h1>Comprehensive SEO/GEO Audit Report</h1>" +
"<p class=\"url\">" + (d.url || "N/A") + "</p>" +
"<p class=\"meta\">Generated by PromptArch Vibe Architect | " + now + "</p></div>";
// === EXECUTIVE SUMMARY ===
html += "<div class=\"section\"><h2><span class=\"icon\">01</span> Executive Summary</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\" style=\"color:" + sc(d.scores?.overall || 0) + "\">" + (d.scores?.overall || 0) + "/100</div><div class=\"stat-label\">Overall Score</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.issues?.length || 0) + "</div><div class=\"stat-label\">Issues Found</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + criticalIssues.length + "</div><div class=\"stat-label\">Critical Issues</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + geoScore + "/100</div><div class=\"stat-label\">GEO Readiness</div></div>" +
"</div></div>";
// === SCORES ===
html += "<div class=\"section\"><h2><span class=\"icon\">02</span> Scoring Breakdown</h2>" +
"<div class=\"scores\">" +
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) +
"<div class=\"score-card\"><div class=\"score-value\" style=\"color:" + geoColor + "\">" + geoScore + "</div><div class=\"score-label\">GEO</div></div>" +
"</div>" +
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) +
"</div>";
// === META TAGS ===
html += "<div class=\"section\"><h2><span class=\"icon\">03</span> Meta Tags Analysis</h2>" +
"<table><tr><th>Element</th><th>Value</th><th>Status</th></tr>" +
"<tr><td><strong>Title</strong></td><td>" + t(d.title, "MISSING") + " <span style=\"color:#64748b\">(" + (d.titleLength || 0) + " chars)</span></td><td>" + statusBadge(d.titleStatus) + "</td></tr>" +
"<tr><td><strong>Meta Description</strong></td><td>" + t(d.metaDescription, "MISSING") + " <span style=\"color:#64748b\">(" + (d.descLength || 0) + " chars)</span></td><td>" + statusBadge(d.descStatus) + "</td></tr>" +
"<tr><td><strong>Meta Keywords</strong></td><td>" + t(d.metaKeywords, "Not set (modern SEO does not require)") + "</td><td><span style=\"color:#64748b;font-size:11px\">Not a ranking factor</span></td></tr>" +
"<tr><td><strong>Viewport</strong></td><td>" + t(d.viewport, "MISSING") + "</td><td>" + passFail(!!d.viewport) + "</td></tr>" +
"<tr><td><strong>Charset</strong></td><td>" + t(d.charset, "MISSING") + "</td><td>" + passFail(!!d.charset) + "</td></tr>" +
"<tr><td><strong>Canonical</strong></td><td>" + (d.canonical || "<em>None</em>") + "</td><td>" + (d.hasCanonicalMismatch ? badge("MISMATCH", "#ef4444") : d.canonical ? badge("OK", "#22c55e") : badge("MISSING", "#ef4444")) + "</td></tr>" +
"<tr><td><strong>Robots</strong></td><td>" + (d.robotsDirectives || "None") + "</td><td><span style=\"color:#64748b;font-size:11px\">-</span></td></tr>" +
"<tr><td><strong>X-Frame-Options</strong></td><td>" + t(d.xFrameOptions, "Not set") + "</td><td>" + passFail(!!d.xFrameOptions) + "</td></tr>" +
"<tr><td><strong>Protocol</strong></td><td>" + (d.protocol || "Unknown") + "</td><td>" + (d.protocol === "HTTPS" ? badge("SECURE", "#22c55e") : badge("INSECURE", "#ef4444")) + "</td></tr>" +
"</table></div>";
// === SOCIAL / OPEN GRAPH ===
html += "<div class=\"section\"><h2><span class=\"icon\">04</span> Social & Open Graph</h2>" +
"<table><tr><th>Property</th><th>Value</th><th>Status</th></tr>" +
"<tr><td><strong>OG Title</strong></td><td>" + t(d.openGraph?.title, "Missing") + "</td><td>" + passFail(!!d.openGraph?.title) + "</td></tr>" +
"<tr><td><strong>OG Description</strong></td><td>" + t(d.openGraph?.description, "Missing") + "</td><td>" + passFail(!!d.openGraph?.description) + "</td></tr>" +
"<tr><td><strong>OG Image</strong></td><td>" + (d.openGraph?.image ? '<a href="' + d.openGraph.image + '" target="_blank" style="font-size:12px">' + d.openGraph.image.substring(0, 80) + '...</a>' : "<em>Missing</em>") + "</td><td>" + passFail(!!d.openGraph?.image) + "</td></tr>" +
"<tr><td><strong>OG Type</strong></td><td>" + t(d.openGraph?.type, "Missing") + "</td><td>" + passFail(!!d.openGraph?.type) + "</td></tr>" +
"<tr><td><strong>OG URL</strong></td><td>" + t(d.openGraph?.url, "Missing") + "</td><td>" + passFail(!!d.openGraph?.url) + "</td></tr>" +
"<tr><td><strong>Twitter Card</strong></td><td>" + t(d.twitterCard?.card, "Not set") + "</td><td>" + passFail(!!d.twitterCard?.card) + "</td></tr>" +
"<tr><td><strong>Twitter Title</strong></td><td>" + t(d.twitterCard?.title, "Missing") + "</td><td>" + passFail(!!d.twitterCard?.title) + "</td></tr>" +
"<tr><td><strong>Twitter Description</strong></td><td>" + t(d.twitterCard?.description, "Missing") + "</td><td>" + passFail(!!d.twitterCard?.description) + "</td></tr>" +
"</table></div>";
// === HEADINGS ===
html += "<div class=\"section\"><h2><span class=\"icon\">05</span> Heading Structure</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h1Count + "</div><div class=\"stat-label\">H1</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h2Count + "</div><div class=\"stat-label\">H2</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h3Count + "</div><div class=\"stat-label\">H3</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + d.h4Count + "</div><div class=\"stat-label\">H4+</div></div>" +
"</div>" +
"<p style=\"margin:10px 0;color:#94a3b8\">Status: " + statusBadge(d.headingStatus) + "</p>" +
(headingRows ? "<p style=\"color:#94a3b8;font-size:12px;margin-bottom:8px\">Heading hierarchy (up to 30 shown):</p><table><tr><th style=\"width:60px\">Level</th><th>Text</th></tr>" + headingRows + "</table>" : "<p style=\"color:#64748b\">No headings found.</p>") +
"</div>";
// === LINKS ===
html += "<div class=\"section\"><h2><span class=\"icon\">06</span> Links Analysis</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.total || 0) + "</div><div class=\"stat-label\">Total Links</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.internal || 0) + "</div><div class=\"stat-label\">Internal</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.external || 0) + "</div><div class=\"stat-label\">External</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.links?.nofollow || 0) + "</div><div class=\"stat-label\">Nofollow</div></div>" +
"</div>";
if (extLinkRows) {
html += "<p style=\"color:#94a3b8;font-size:12px;margin-bottom:8px\">Sample external links:</p>" +
"<table><tr><th>URL</th><th>Anchor Text</th><th>Rel</th></tr>" + extLinkRows + "</table>";
}
html += "</div>";
// === IMAGES ===
html += "<div class=\"section\"><h2><span class=\"icon\">07</span> Images & Alt Text</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.total || 0) + "</div><div class=\"stat-label\">Total Images</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.withAlt || 0) + "</div><div class=\"stat-label\">With Alt</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.withoutAlt || 0) + "</div><div class=\"stat-label\">Missing Alt</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.images?.altCoverage || 0) + "%</div><div class=\"stat-label\">Alt Coverage</div></div>" +
"</div>" +
"<p style=\"margin:10px 0\">Lazy Loaded: " + passFail((d.images?.lazyLoaded || 0) > 0) + " (" + (d.images?.lazyLoaded || 0) + " images)</p>";
if (altRows) {
html += "<p style=\"color:#f59e0b;font-size:12px;margin-bottom:8px\">Images missing alt text:</p>" +
"<table><tr><th>Image Source</th></tr>" + altRows + "</table>";
}
html += "</div>";
// === CONTENT ===
html += "<div class=\"section\"><h2><span class=\"icon\">08</span> Content Analysis</h2>" +
"<div class=\"stat-grid\">" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.wordCount || 0) + "</div><div class=\"stat-label\">Word Count</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.sentenceCount || 0) + "</div><div class=\"stat-label\">Sentences</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.paragraphCount || 0) + "</div><div class=\"stat-label\">Paragraphs</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.content?.avgWordsPerSentence || 0) + "</div><div class=\"stat-label\">Avg Words/Sentence</div></div>" +
"</div>" +
"<table><tr><th>Metric</th><th>Value</th><th>Recommendation</th></tr>" +
"<tr><td>Word Count</td><td>" + (d.content?.wordCount || 0) + "</td><td>" + ((d.content?.wordCount || 0) >= 1000 ? badge("GOOD", "#22c55e") : badge("BELOW 1000", "#f59e0b")) + " Aim for 1000+ on core pages</td></tr>" +
"<tr><td>Readability</td><td>" + (d.content?.avgWordsPerSentence || 0) + " avg words/sentence</td><td>" + ((d.content?.avgWordsPerSentence || 0) <= 20 ? badge("GOOD", "#22c55e") : badge("LONG", "#f59e0b")) + " Keep under 20 for readability</td></tr>" +
"</table></div>";
// === PERFORMANCE ===
html += "<div class=\"section\"><h2><span class=\"icon\">09</span> Performance Signals</h2>" +
"<table><tr><th>Metric</th><th>Value</th><th>Status</th></tr>" +
"<tr><td><strong>Server</strong></td><td>" + (d.server || "Unknown") + "</td><td>-</td></tr>" +
"<tr><td><strong>Response Time</strong></td><td>" + d.responseTime + "ms</td><td>" + (d.responseTime < 500 ? badge("FAST", "#22c55e") : d.responseTime < 1500 ? badge("OK", "#f59e0b") : badge("SLOW", "#ef4444")) + "</td></tr>" +
"<tr><td><strong>HTML Size</strong></td><td>" + (d.htmlSize || 0).toLocaleString() + " bytes</td><td>-</td></tr>" +
"<tr><td><strong>Content Encoding</strong></td><td>" + t(d.performance?.contentEncoding, "None detected") + "</td><td>" + passFail(!!d.performance?.contentEncoding) + "</td></tr>" +
"<tr><td><strong>External Scripts</strong></td><td>" + (d.performance?.externalScripts || 0) + "</td><td>" + ((d.performance?.externalScripts || 0) <= 5 ? badge("OK", "#22c55e") : badge("HIGH", "#f59e0b")) + "</td></tr>" +
"<tr><td><strong>External Stylesheets</strong></td><td>" + (d.performance?.externalStylesheets || 0) + "</td><td>-</td></tr>" +
"<tr><td><strong>Inline Styles</strong></td><td>" + (d.performance?.inlineStyles || 0) + "</td><td>" + ((d.performance?.inlineStyles || 0) <= 10 ? badge("OK", "#22c55e") : badge("HIGH", "#f59e0b")) + "</td></tr>" +
"<tr><td><strong>Inline Scripts</strong></td><td>" + (d.performance?.inlineScripts || 0) + "</td><td>" + ((d.performance?.inlineScripts || 0) === 0 ? badge("GOOD", "#22c55e") : badge("FOUND", "#f59e0b")) + "</td></tr>" +
"<tr><td><strong>Preconnect</strong></td><td>" + (d.performance?.hasPreconnect ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.hasPreconnect) + "</td></tr>" +
"<tr><td><strong>Preload</strong></td><td>" + (d.performance?.hasPreload ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.hasPreload) + "</td></tr>" +
"<tr><td><strong>DNS Prefetch</strong></td><td>" + (d.performance?.hasDnsPrefetch ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.hasDnsPrefetch) + "</td></tr>" +
"<tr><td><strong>Async/Defer Scripts</strong></td><td>" + (d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts ? "Yes" : "No") + "</td><td>" + passFail(d.performance?.usesAsyncScripts || d.performance?.usesDeferScripts) + "</td></tr>" +
"</table></div>";
// === ACCESSIBILITY ===
html += "<div class=\"section\"><h2><span class=\"icon\">10</span> Accessibility</h2>" +
"<table><tr><th>Check</th><th>Status</th><th>Impact</th></tr>" +
"<tr><td><strong>HTML lang attribute</strong></td><td>" + passFail(d.accessibility?.hasLangAttr) + "</td><td>Screen readers, SEO</td></tr>" +
"<tr><td><strong>ARIA labels present</strong></td><td>" + passFail(d.accessibility?.hasAriaLabels) + "</td><td>Assistive technology</td></tr>" +
"<tr><td><strong>First image has alt text</strong></td><td>" + passFail(d.accessibility?.hasAltOnFirstImage) + "</td><td>Screen readers</td></tr>" +
"<tr><td><strong>Alt text coverage</strong></td><td>" + (d.images?.altCoverage || 0) + "%</td><td>" + ((d.images?.altCoverage || 0) >= 90 ? badge("GOOD", "#22c55e") : (d.images?.altCoverage || 0) >= 70 ? badge("FAIR", "#f59e0b") : badge("POOR", "#ef4444")) + "</td></tr>" +
"</table></div>";
// === STRUCTURED DATA ===
html += "<div class=\"section\"><h2><span class=\"icon\">11</span> Structured Data / Schema</h2>" +
"<table><tr><th>Format</th><th>Status</th></tr>" +
"<tr><td><strong>JSON-LD</strong></td><td>" + passFail(d.structuredData?.hasJsonLd) + "</td></tr>" +
"<tr><td><strong>Microdata</strong></td><td>" + passFail(d.structuredData?.hasMicrodata) + "</td></tr>" +
"</table>";
if (sdRows) {
html += "<p style=\"color:#94a3b8;font-size:12px;margin:10px 0\">Detected schema types:</p>" +
"<table><tr><th>Schema Type</th><th>Found</th></tr>" + sdRows + "</table>";
} else if (!d.structuredData?.hasJsonLd && !d.structuredData?.hasMicrodata) {
html += "<p style=\"color:#f59e0b;font-size:12px;margin-top:10px\">No structured data detected. Adding JSON-LD schema (FAQ, Article, Organization, Product) can significantly improve search visibility and enable rich results.</p>";
}
html += "</div>";
// === HREFLANG ===
if (hreflangTags.length > 0) {
html += "<div class=\"section\"><h2><span class=\"icon\">12</span> Hreflang Tags</h2>" +
"<table><tr><th>Language-Region</th></tr>" + hreflangRows + "</table></div>";
}
// === GEO ANALYSIS ===
html += "<div class=\"section\"><h2><span class=\"icon\">13</span> GEO (Generative Engine Optimization)</h2>" +
"<div class=\"stat-grid\"><div class=\"stat-item\"><div class=\"stat-value\" style=\"color:" + geoColor + "\">" + geoScore + "/100</div><div class=\"stat-label\">GEO Readiness Score</div></div>" +
"<div class=\"stat-item\"><div class=\"stat-value\">" + (d.structuredData?.hasJsonLd ? "Yes" : "No") + "</div><div class=\"stat-label\">Schema Markup</div></div></div>" +
geoBar("Factual Content Structure", geoFactualScore(d)) +
geoBar("Entity Schema", geoEntityScore(d)) +
geoBar("Content Depth", geoContentDepthScore(d)) +
geoBar("Citeability", geoCiteabilityScore(d)) +
"<h3 style=\"font-size:14px;margin:16px 0 10px;color:#94a3b8\">What is GEO?</h3>" +
"<p style=\"font-size:13px;color:#94a3b8;margin-bottom:12px\">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.</p>" +
"<h3 style=\"font-size:14px;margin:16px 0 10px;color:#94a3b8\">GEO Improvement Checklist</h3>" +
"<table><tr><th>Factor</th><th>Status</th><th>Action</th></tr>" +
"<tr><td>Structured Data / Schema</td><td>" + passFail(d.structuredData?.hasJsonLd) + "</td><td>Add JSON-LD for Organization, FAQ, Article, Product schemas</td></tr>" +
"<tr><td>Heading Hierarchy</td><td>" + statusBadge(d.headingStatus) + "</td><td>One H1, logical H2-H4 hierarchy with keyword-rich headings</td></tr>" +
"<tr><td>Content Depth (1000+ words)</td><td>" + passFail((d.content?.wordCount || 0) >= 1000) + "</td><td>Expand core pages with comprehensive, authoritative content</td></tr>" +
"<tr><td>FAQ / Q&A Format</td><td>" + passFail(d.structuredData?.types?.some((s) => s.type === "FAQ") && d.structuredData?.types?.some((s) => s.found)) + "</td><td>Add FAQ schema with common questions about your topic</td></tr>" +
"<tr><td>Clear Facts & Statistics</td><td>" + passFail((d.content?.wordCount || 0) >= 500) + "</td><td>Include verifiable data points, statistics, and citations</td></tr>" +
"<tr><td>Lists & Tables</td><td>" + passFail(d.h2Count >= 3) + "</td><td>Use structured lists and comparison tables for scannability</td></tr>" +
"<tr><td>Authoritative Tone</td><td>" + passFail(d.h1Count === 1) + "</td><td>Write expert-level content with clear author attribution</td></tr>" +
"<tr><td>Meta Description CTA</td><td>" + passFail(!!d.metaDescription && d.metaDescription.length >= 120) + "</td><td>Include clear call-to-action in meta descriptions</td></tr>" +
"</table></div>";
// === ISSUES ===
html += "<div class=\"section\"><h2><span class=\"icon\">14</span> Issues & How to Fix (" + (d.issues?.length || 0) + " found)</h2>" +
"<div style=\"display:flex;gap:8px;margin-bottom:14px\">" +
"<span style=\"display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700\">" + badge("CRITICAL: " + criticalIssues.length, "#ef4444") + "</span>" +
"<span style=\"display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700\">" + badge("WARNING: " + warningIssues.length, "#f59e0b") + "</span>" +
"<span style=\"display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700\">" + badge("INFO: " + infoIssues.length, "#6b7280") + "</span>" +
"</div>" +
"<table><tr><th>Severity</th><th>Category</th><th>Issue</th><th>How to Fix</th></tr>" +
(allIssueRows || "<tr><td colspan=\"4\" style=\"text-align:center;color:#22c55e;padding:20px\">No issues detected. Great job!</td></tr>") +
"</table></div>";
// === ACTION PLAN ===
html += "<div class=\"section\"><h2><span class=\"icon\">15</span> Prioritized Action Plan</h2>" +
"<p style=\"color:#94a3b8;font-size:12px;margin-bottom:14px\">Recommended fixes ordered by impact and priority.</p>" +
actionPlan + "</div>";
// === FAQ RECOMMENDATIONS ===
html += "<div class=\"section\"><h2><span class=\"icon\">16</span> Recommended FAQ Schema Questions</h2>" +
"<p style=\"color:#94a3b8;font-size:12px;margin-bottom:14px\">Based on your content analysis, consider adding these FAQ items to improve featured snippet eligibility and GEO performance.</p>" +
generateFaqRecommendations(d) + "</div>";
// === GENERAL RECOMMENDATIONS ===
html += "<div class=\"section\"><h2><span class=\"icon\">17</span> SEO/GEO Best Practices Checklist</h2><ol style=\"padding-left:20px\">" +
"<li><strong>Title Tags:</strong> Keep under 60 characters. Place primary keyword near the beginning. Make each page title unique.</li>" +
"<li><strong>Meta Descriptions:</strong> Write 150-160 characters with a clear call-to-action. Include target keywords naturally.</li>" +
"<li><strong>Heading Structure:</strong> Use exactly one H1 per page. Create a logical hierarchy. Include keywords in headings.</li>" +
"<li><strong>Content Quality:</strong> Aim for 1000+ words on core pages. Write for users first, search engines second. Update content regularly.</li>" +
"<li><strong>Image Optimization:</strong> Add descriptive alt text to every image. Use WebP/AVIF format. Implement lazy loading. Compress file sizes.</li>" +
"<li><strong>Page Speed:</strong> Minimize render-blocking resources. Enable compression (Brotli/gzip). Use CDN. Optimize images and fonts.</li>" +
"<li><strong>Mobile Experience:</strong> Ensure responsive design. Test tap targets. Avoid horizontal scrolling. Optimize font sizes.</li>" +
"<li><strong>Structured Data:</strong> Implement JSON-LD schema markup. Validate with Google Rich Results Test. Add FAQ, Article, or Product schema.</li>" +
"<li><strong>Internal Linking:</strong> Create logical site architecture. Use descriptive anchor text. Ensure important pages are within 3 clicks of homepage.</li>" +
"<li><strong>Technical SEO:</strong> Submit XML sitemap. Configure robots.txt properly. Fix broken links. Implement canonical tags. Monitor crawl errors.</li>" +
"<li><strong>GEO Optimization:</strong> 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.</li>" +
"<li><strong>Security:</strong> Enforce HTTPS. Set HSTS headers. Configure X-Frame-Options. Implement Content-Security-Policy.</li>" +
"</ol></div>";
// === FOOTER ===
html += "<div class=\"footer\"><p>PromptArch Vibe Architect | " + now + "</p>" +
"<p><a href=\"https://rommark.dev/tools/promptarch/\">rommark.dev/tools/promptarch</a></p></div>";
html += "</div></body></html>";
return html;
}
// --- Helper functions ---
function scoreCard(label: string, value: number): string {
return "<div class=\"score-card\"><div class=\"score-value\" style=\"color:" + sc(value) + "\">" + value + "</div><div class=\"score-label\">" + label + "</div></div>";
}
function geoBar(label: string, value: number): string {
return "<div style=\"margin:8px 0\"><div style=\"display:flex;justify-content:space-between;font-size:12px;margin-bottom:4px\"><span>" + label + "</span><span style=\"font-weight:700;color:" + sc(value) + "\">" + value + "%</span></div>" +
"<div class=\"geo-bar\"><div class=\"geo-fill\" style=\"width:" + value + "%;background:" + sc(value) + "\"></div></div></div>";
}
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 <html> 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<string>();
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 =>
"<div class=\"action-item\"><div><span class=\"action-priority " + item.priority + "\">" + item.priority + "</span></div>" +
"<div><div style=\"font-weight:600;font-size:13px\">" + item.action + "</div>" +
"<div style=\"font-size:11px;color:#94a3b8;margin-top:2px\">Impact: " + item.impact + "</div></div></div>"
).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 + "?<br>Provide a clear, concise answer (40-60 words) explaining what " + domain + " offers and its primary value proposition.");
questions.push("How does " + domain + " work?<br>Explain the core functionality, process, or service in simple terms.");
questions.push("What are the main benefits of using " + domain + "?<br>List 3-5 key benefits with brief explanations.");
questions.push("Is " + domain + " free or paid?<br>Provide clear pricing information or state if the service is free.");
questions.push("How do I get started with " + domain + "?<br>Outline the steps a new user should take to begin.");
if (d.protocol === "HTTPS") {
questions.push("Is " + domain + " secure?<br>Confirm security measures in place (SSL, data protection, etc.).");
}
if ((d.content?.wordCount || 0) >= 300) {
questions.push("What makes " + domain + " different from competitors?<br>Highlight unique selling points and differentiators.");
}
return questions.map((q, i) => {
const parts = q.split("<br>");
return "<div style=\"padding:12px 0;border-bottom:1px solid #334155\"><div style=\"font-weight:700;font-size:13px;color:#e2e8f0\">" + (i + 1) + ". " + parts[0] + "</div>" +
"<div style=\"font-size:12px;color:#94a3b8;margin-top:4px\">" + parts[1] + "</div></div>";
}).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);
};
}
}
}