feat: SEO audit export, default Vibe Architect, /vibe route (v2.0.0)
This commit is contained in:
19
CHANGELOG.md
19
CHANGELOG.md
@@ -3,7 +3,24 @@
|
||||
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](https://semver.org/spec/v2.0.0.html).
|
||||
and this project adheres to [Semantic Versioning]
|
||||
## [2.0.0] - 2026-03-18 21:33 UTC
|
||||
|
||||
### Added
|
||||
- **SEO Audit Export** — Export SEO/GEO audit reports as HTML or PDF with comprehensive fix instructions
|
||||
- **SEO Report Generator** — Standalone `lib/seo-report.ts` utility with color-coded scores, issue tables with fix instructions, and print-friendly CSS
|
||||
- **Default Vibe Architect** — Welcome screen now opens to Vibe Architect by default (was Prompt Enhancer)
|
||||
- **Vibe Architect Dedicated Route** — Full-screen immersive mode at `/vibe` with `vibeMode` prop
|
||||
|
||||
### Changed
|
||||
- **General Agent Plain Chat** — General mode no longer triggers plan/code flow, now works as plain chat
|
||||
- **SEO Follow-up Fix** — Non-visual agents (SEO, content, SMM) preserve existing canvas on follow-up messages
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 3 (AIAssist.tsx, page.tsx)
|
||||
- Files added: 2 (seo-report.ts, vibe/page.tsx)
|
||||
|
||||
(https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.9.0] - 2026-03-18 21:05 UTC
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PromptArch: AI Orchestration Platform
|
||||
|
||||
> **Latest Version**: [v1.9.0](CHANGELOG.md#190---2026-03-18) (2026-03-18)
|
||||
> **Latest Version**: [v2.0.0](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.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.8.0](CHANGELOG.md#180---2026-03-18) | 2026-03-18 21:02 UTC | Vibe Architect dedicated mode, SEO follow-up fix |
|
||||
| [1.7.0](CHANGELOG.md#170---2026-03-18) | 2026-03-18 20:44 UTC | Industry-grade SEO audit, plan flow fix for non-code agents |
|
||||
|
||||
@@ -20,7 +20,7 @@ const HistoryPanel = dynamic(() => import("@/components/HistoryPanel"), { ssr: f
|
||||
const SettingsPanel = dynamic(() => import("@/components/SettingsPanel"), { ssr: false });
|
||||
|
||||
export default function Home() {
|
||||
const [currentView, setCurrentView] = useState<View>("enhance");
|
||||
const [currentView, setCurrentView] = useState<View>("ai-assist");
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[Home] Initializing Qwen OAuth service on client...");
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, memo } from "react";
|
||||
import { downloadSeoReport } from "@/lib/seo-report";
|
||||
import {
|
||||
MessageSquare, Send, Code2, Palette, Search,
|
||||
Trash2, Copy, Monitor, StopCircle, X, Zap, Ghost,
|
||||
Wand2, LayoutPanelLeft, Play, Orbit, Plus, Key, ShieldCheck, Wrench
|
||||
Wand2, LayoutPanelLeft, Play, Orbit, Plus, Key, ShieldCheck, Wrench, FileText
|
||||
} from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@@ -520,6 +521,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
|
||||
const [deviceSize, setDeviceSize] = useState<"full" | "desktop" | "tablet" | "mobile">("full");
|
||||
const deviceWidths: Record<string, string> = { full: "100%", desktop: "1280px", tablet: "768px", mobile: "375px" };
|
||||
const [isModifying, setIsModifying] = useState(false);
|
||||
const [seoAuditData, setSeoAuditData] = useState<any>(null);
|
||||
const [abortController, setAbortController] = useState<AbortController | null>(null);
|
||||
|
||||
// Agent suggestion state
|
||||
@@ -711,6 +713,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
|
||||
const auditRes = await fetch("/api/fetch-url?url=" + encodeURIComponent(url));
|
||||
if (auditRes.ok) {
|
||||
const d = await auditRes.json();
|
||||
setSeoAuditData(d);
|
||||
enrichedInput += "\n\n[COMPREHENSIVE SEO AUDIT - " + url + "]\n";
|
||||
enrichedInput += "== OVERALL SCORE: " + (d.scores?.overall || "?") + "/100 ==\n";
|
||||
enrichedInput += "Technical: " + (d.scores?.technical || "?") + " | Content: " + (d.scores?.content || "?") + " | Performance: " + (d.scores?.performance || "?") + " | Social: " + (d.scores?.social || "?") + "\n\n";
|
||||
@@ -911,6 +914,11 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
const exportSeoReport = (format: "html" | "pdf") => {
|
||||
if (!seoAuditData) return;
|
||||
downloadSeoReport(seoAuditData, format);
|
||||
};
|
||||
|
||||
|
||||
const clearHistory = () => {
|
||||
updateActiveTab({
|
||||
@@ -1338,6 +1346,7 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
|
||||
|
||||
{/* Post-coding action buttons */}
|
||||
{msg.role === "assistant" && assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing && (
|
||||
<>
|
||||
<div className="mt-4 grid grid-cols-3 gap-2 animate-in zoom-in-95 duration-300">
|
||||
<Button
|
||||
onClick={() => { setShowCanvas(true); setViewMode(isPreviewRenderable(previewData as PreviewData) ? "preview" : "code"); }}
|
||||
@@ -1361,6 +1370,25 @@ export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } =
|
||||
<LayoutPanelLeft className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Modify</span>
|
||||
</Button>
|
||||
</div>
|
||||
{currentAgent === "seo" && seoAuditData && (
|
||||
<div className="mt-2 flex gap-2 animate-in zoom-in-95 duration-300">
|
||||
<Button
|
||||
onClick={() => exportSeoReport("html")}
|
||||
variant="outline"
|
||||
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/20 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
|
||||
>
|
||||
<Download className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export HTML</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => exportSeoReport("pdf")}
|
||||
variant="outline"
|
||||
className="flex-1 bg-amber-500/10 hover:bg-amber-500/20 border-amber-500/20 text-amber-300 font-black uppercase text-[9px] tracking-wider py-3 rounded-xl min-w-0"
|
||||
>
|
||||
<FileText className="h-3.5 w-3.5 mr-1" /> <span className="truncate">Export PDF</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
250
lib/seo-report.ts
Normal file
250
lib/seo-report.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* 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 win = window.open("", "_blank");
|
||||
if (win) {
|
||||
win.document.write(html);
|
||||
win.document.close();
|
||||
setTimeout(() => win.print(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user