From 32f01320023831499794526ec7a62f8ba22183b5 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 18 Mar 2026 21:38:21 +0000 Subject: [PATCH] fix: PDF export using Blob URL to bypass popup blockers --- lib/seo-report.ts | 503 +++++++++++++++++++++++----------------------- 1 file changed, 253 insertions(+), 250 deletions(-) diff --git a/lib/seo-report.ts b/lib/seo-report.ts index 05cea2e..33fb035 100644 --- a/lib/seo-report.ts +++ b/lib/seo-report.ts @@ -1,250 +1,253 @@ -/** - * SEO Audit Report Generator - * Generates standalone HTML reports from audit data - */ - -export interface SeoAuditData { - url: string; - domain: string; - responseTime: number; - server: string; - htmlSize: number; - title: string | null; - titleLength: number; - titleStatus: string; - metaDescription: string | null; - descLength: number; - descStatus: string; - metaKeywords: string | null; - viewport: string | null; - charset: string | null; - robotsDirectives: string | null; - canonical: string | null; - hasCanonicalMismatch: boolean; - h1Count: number; - h2Count: number; - h3Count: number; - h4Count: number; - headingStatus: string; - headings: { level: number; text: string }[]; - links?: { total: number; internal: number; external: number; nofollow: number }; - images?: { total: number; withAlt: number; withoutAlt: number; lazyLoaded: number; altCoverage: number }; - content?: { wordCount: number; sentenceCount: number; paragraphCount: number; avgWordsPerSentence: number }; - openGraph?: { title: string | null; description: string | null; image: string | null; type: string | null }; - twitterCard?: { card: string | null }; - performance?: { - inlineStyles: number; - externalScripts: number; - externalStylesheets: number; - hasPreconnect: boolean; - hasPreload: boolean; - hasDnsPrefetch: boolean; - usesAsyncScripts: boolean; - usesDeferScripts: boolean; - }; - accessibility?: { hasLangAttr: boolean; hasAriaLabels: boolean; hasAltOnFirstImage: boolean }; - structuredData?: { hasJsonLd: boolean; hasMicrodata: boolean; types: { type: string; found: boolean }[] }; - scores?: { overall: number; technical: number; content: number; performance: number; social: number }; - issues?: { severity: string; category: string; message: string }[]; -} - -const scoreColor = (s: number): string => - s >= 80 ? "#22c55e" : s >= 60 ? "#f59e0b" : "#ef4444"; - -const sevColor = (s: string): string => - s === "critical" ? "#ef4444" : s === "warning" ? "#f59e0b" : "#6b7280"; - -const fixInstructions: Record = { - Meta: "Ensure every page has a unique title tag (50-60 chars) with primary keyword near start. Write meta descriptions (150-160 chars) with CTA. Add viewport meta tag.", - Content: "Maintain exactly one H1 per page with primary keyword. Build logical heading hierarchy (H1>H2>H3). Aim for 1000+ words on core pages.", - Technical: "Set self-referencing canonical tags. Ensure valid SSL with HTTPS redirect. Check robots.txt is not blocking important pages.", - Mobile: "Add viewport meta tag. Test with Google Mobile-Friendly tool. Ensure tap targets are 48x48px minimum.", - Security: "Migrate to HTTPS with valid SSL. Set up HTTP-to-HTTPS redirects. Configure HSTS headers.", - Performance: "Minimize inline styles. Add preconnect hints. Implement lazy loading. Use async/defer for scripts. Compress images.", - Social: "Add OG tags (og:title, og:description, og:image). Add Twitter Card meta. Ensure OG images are 1200x630px+.", - Accessibility: "Add lang attribute to . Implement ARIA labels. Add descriptive alt text to all images.", - Links: "Fix broken internal links. Add external links to authoritative sources. Review nofollow attributes.", - "Structured Data": "Implement JSON-LD schema. Validate with Google Rich Results Test. Add FAQ/Article/Product schema.", -}; - -export function generateSeoReportHtml(d: SeoAuditData): string { - const now = new Date().toLocaleString(); - const sc = scoreColor; - const svc = sevColor; - - const issueRows = (d.issues || []) - .map( - (issue) => - "" + - issue.severity.toUpperCase() + - "" + - issue.category + - "" + - issue.message + - "" + - (fixInstructions[issue.category] || "Review and address this issue.") + - "" - ) - .join(""); - - const headingRows = (d.headings || []) - .map((h) => "H" + h.level + "" + h.text + "") - .join(""); - - const t = (v: string | null | undefined, fallback = "None") => v || "" + fallback + ""; - - return "SEO Audit - " + - (d.domain || "report") + - "

SEO/GEO Audit Report

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

PromptArch Vibe Architect | " + - now + - "

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

Meta Tags

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

Social / Open Graph

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

Headings

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

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

Links & Images

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

Content

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

Performance

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

Issues & How to Fix

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

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

SEO/GEO Recommendations

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

PromptArch Vibe Architect | " + - now + - "

rommark.dev/tools/promptarch

"; -} - -export function downloadSeoReport(d: SeoAuditData, format: "html" | "pdf") { - const html = generateSeoReportHtml(d); - - if (format === "html") { - const blob = new Blob([html], { type: "text/html" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "seo-audit-" + (d.domain || "report") + "-" + new Date().toISOString().slice(0, 10) + ".html"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - } else { - const win = window.open("", "_blank"); - if (win) { - win.document.write(html); - win.document.close(); - setTimeout(() => win.print(), 500); - } - } -} +/** + * SEO Audit Report Generator + * Generates standalone HTML reports from audit data + */ + +export interface SeoAuditData { + url: string; + domain: string; + responseTime: number; + server: string; + htmlSize: number; + title: string | null; + titleLength: number; + titleStatus: string; + metaDescription: string | null; + descLength: number; + descStatus: string; + metaKeywords: string | null; + viewport: string | null; + charset: string | null; + robotsDirectives: string | null; + canonical: string | null; + hasCanonicalMismatch: boolean; + h1Count: number; + h2Count: number; + h3Count: number; + h4Count: number; + headingStatus: string; + headings: { level: number; text: string }[]; + links?: { total: number; internal: number; external: number; nofollow: number }; + images?: { total: number; withAlt: number; withoutAlt: number; lazyLoaded: number; altCoverage: number }; + content?: { wordCount: number; sentenceCount: number; paragraphCount: number; avgWordsPerSentence: number }; + openGraph?: { title: string | null; description: string | null; image: string | null; type: string | null }; + twitterCard?: { card: string | null }; + performance?: { + inlineStyles: number; + externalScripts: number; + externalStylesheets: number; + hasPreconnect: boolean; + hasPreload: boolean; + hasDnsPrefetch: boolean; + usesAsyncScripts: boolean; + usesDeferScripts: boolean; + }; + accessibility?: { hasLangAttr: boolean; hasAriaLabels: boolean; hasAltOnFirstImage: boolean }; + structuredData?: { hasJsonLd: boolean; hasMicrodata: boolean; types: { type: string; found: boolean }[] }; + scores?: { overall: number; technical: number; content: number; performance: number; social: number }; + issues?: { severity: string; category: string; message: string }[]; +} + +const scoreColor = (s: number): string => + s >= 80 ? "#22c55e" : s >= 60 ? "#f59e0b" : "#ef4444"; + +const sevColor = (s: string): string => + s === "critical" ? "#ef4444" : s === "warning" ? "#f59e0b" : "#6b7280"; + +const fixInstructions: Record = { + Meta: "Ensure every page has a unique title tag (50-60 chars) with primary keyword near start. Write meta descriptions (150-160 chars) with CTA. Add viewport meta tag.", + Content: "Maintain exactly one H1 per page with primary keyword. Build logical heading hierarchy (H1>H2>H3). Aim for 1000+ words on core pages.", + Technical: "Set self-referencing canonical tags. Ensure valid SSL with HTTPS redirect. Check robots.txt is not blocking important pages.", + Mobile: "Add viewport meta tag. Test with Google Mobile-Friendly tool. Ensure tap targets are 48x48px minimum.", + Security: "Migrate to HTTPS with valid SSL. Set up HTTP-to-HTTPS redirects. Configure HSTS headers.", + Performance: "Minimize inline styles. Add preconnect hints. Implement lazy loading. Use async/defer for scripts. Compress images.", + Social: "Add OG tags (og:title, og:description, og:image). Add Twitter Card meta. Ensure OG images are 1200x630px+.", + Accessibility: "Add lang attribute to . Implement ARIA labels. Add descriptive alt text to all images.", + Links: "Fix broken internal links. Add external links to authoritative sources. Review nofollow attributes.", + "Structured Data": "Implement JSON-LD schema. Validate with Google Rich Results Test. Add FAQ/Article/Product schema.", +}; + +export function generateSeoReportHtml(d: SeoAuditData): string { + const now = new Date().toLocaleString(); + const sc = scoreColor; + const svc = sevColor; + + const issueRows = (d.issues || []) + .map( + (issue) => + "" + + issue.severity.toUpperCase() + + "" + + issue.category + + "" + + issue.message + + "" + + (fixInstructions[issue.category] || "Review and address this issue.") + + "" + ) + .join(""); + + const headingRows = (d.headings || []) + .map((h) => "H" + h.level + "" + h.text + "") + .join(""); + + const t = (v: string | null | undefined, fallback = "None") => v || "" + fallback + ""; + + return "SEO Audit - " + + (d.domain || "report") + + "

SEO/GEO Audit Report

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

PromptArch Vibe Architect | " + + now + + "

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

Meta Tags

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

Social / Open Graph

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

Headings

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

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

Links & Images

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

Content

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

Performance

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

Issues & How to Fix

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

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

SEO/GEO Recommendations

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

PromptArch Vibe Architect | " + + now + + "

rommark.dev/tools/promptarch

"; +} + +export function downloadSeoReport(d: SeoAuditData, format: "html" | "pdf") { + const html = generateSeoReportHtml(d); + + if (format === "html") { + const blob = new Blob([html], { type: "text/html" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "seo-audit-" + (d.domain || "report") + "-" + new Date().toISOString().slice(0, 10) + ".html"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } else { + const blob = new Blob([html], { type: "text/html" }); + const url = URL.createObjectURL(blob); + const win = window.open(url, "_blank"); + if (win) { + win.addEventListener("load", () => { + setTimeout(() => win.print(), 300); + }); + } + setTimeout(() => URL.revokeObjectURL(url), 60000); + } +}