feat: comprehensive 17-section SEO/GEO audit report with GEO scoring (v2.1.0)
This commit is contained in:
29
CHANGELOG.md
29
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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user