Compare commits
25 Commits
7266920593
...
master
1
.gitignore
vendored
1
.gitignore
vendored
@@ -37,3 +37,4 @@ next-env.d.ts
|
||||
# logs
|
||||
logs
|
||||
*.log
|
||||
fix_*.py
|
||||
|
||||
182
CHANGELOG.md
182
CHANGELOG.md
@@ -3,7 +3,187 @@
|
||||
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.2.1] - 2026-03-19 05:24 UTC
|
||||
|
||||
### Added
|
||||
- **Leads Canvas Preview** - Leads Finder now renders results as an Excel-like HTML table in the canvas, with columns: #, Name, Platform, Followers, Region, Bio, Link
|
||||
- **Dark-Themed Leads Report** - Beautiful dark UI with emerald accents, stats grid (total leads, combined reach, top platform, top region), platform-colored badges (Instagram, Twitter, LinkedIn, YouTube, TikTok), and hover effects
|
||||
- **Auto Canvas** - Leads Finder is now a visual agent that auto-opens the canvas when preview data is available
|
||||
|
||||
### Fixed
|
||||
- **Build Error** - Restored `<Badge variant="outline">` for plan card tech stack divs that were incorrectly replaced with plain `<div>` by an over-broad fix script
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 2 (AIAssist.tsx, openrouter.ts)
|
||||
|
||||
## [2.2.0] - 2026-03-19 04:44 UTC
|
||||
|
||||
### Added
|
||||
- **Leads Finder Agent** - New agent mode in Vibe Architect that finds relevant influencers, prospects, and leads across social media platforms (Instagram, Twitter/X, LinkedIn, YouTube, TikTok)
|
||||
- **Auto Web Search** - Leads Finder automatically enables web search to find real-time leads data
|
||||
- **Structured Lead Format** - Returns leads in consistent format: Name | Followers | Region | Location with description and social URL
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 3 (AIAssist.tsx, translations.ts, openrouter.ts)
|
||||
|
||||
|
||||
## [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
|
||||
- **PDF Export** — Replaced `window.open` with hidden iframe approach for reliable print dialog trigger (no popup blocker issues)
|
||||
- **PDF Export** — Initial fix using Blob URL (superseded by iframe approach)
|
||||
|
||||
### Added
|
||||
- **Inline SEO Export Buttons** — Export HTML/PDF buttons now appear directly in chat messages after SEO audit, not just in post-coding action bar
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 2 (AIAssist.tsx, seo-report.ts)
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
### Changed
|
||||
- **Global Rebrand: AI Assist -> Vibe Architect** — All instances renamed across UI, sidebar, translations (EN/RU/HE)
|
||||
- **Sidebar Highlight** — Vibe Architect button now has a gradient blue-purple border and bold blue text when not active, making it stand out in the navigation menu
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 3 (Sidebar.tsx, AIAssist.tsx, translations.ts)
|
||||
|
||||
## [1.8.0] - 2026-03-18 21:02 UTC
|
||||
|
||||
### Added
|
||||
- **Vibe Architect Dedicated Mode** — Full-screen immersive AI coding experience
|
||||
- New route: `/vibe` ([rommark.dev/tools/promptarch/vibe/](https://rommark.dev/tools/promptarch/vibe/))
|
||||
- No sidebar, no navigation — just AI Assist in full screen
|
||||
- Rebranded as "Vibe Architect" with dedicated messaging
|
||||
- `vibeMode` prop on AIAssist component for label overrides
|
||||
|
||||
### Fixed
|
||||
- **SEO follow-up preview** — Canvas no longer breaks when asking follow-up questions after SEO audit
|
||||
- `isModifying` overlay only triggers for visual agents (code, web, app, design) — not SEO/content/SMM
|
||||
- Non-visual agent follow-ups preserve existing canvas if no new preview is generated
|
||||
|
||||
### Technical Details
|
||||
- Files added: 1 (`app/vibe/page.tsx`)
|
||||
- Files modified: 1 (AIAssist.tsx: +8/-3 lines)
|
||||
- New prop: `vibeMode` on AIAssist component
|
||||
|
||||
## [1.7.0] - 2026-03-18 20:44 UTC
|
||||
|
||||
### Added
|
||||
- **Comprehensive SEO Audit Engine** — Industry-grade URL analysis modeled after Screaming Frog, Ahrefs, Sitebulb, and Semrush
|
||||
- Full meta tag analysis: title (length/status), description (length/status), keywords, viewport, charset, robots directives
|
||||
- Open Graph & Twitter Card validation
|
||||
- Heading structure audit: H1 count check, hierarchy analysis (H1-H6), multiple H1 detection
|
||||
- Link analysis: internal/external/nofollow counts, external link sampling
|
||||
- Image audit: total count, alt text coverage %, lazy loading detection, missing alt samples
|
||||
- Content analysis: word count, sentence count, paragraph count, avg words/sentence
|
||||
- Structured data detection: JSON-LD, Microdata, 12+ schema types (Article, FAQ, Product, LocalBusiness, etc.)
|
||||
- Performance signals: server info, response time, HTML size, external scripts/stylesheets, inline styles, preconnect/preload/dns-prefetch, async/defer detection
|
||||
- Accessibility checks: lang attribute, ARIA labels, first image alt text
|
||||
- Hreflang tag detection for international SEO
|
||||
- Canonical tag validation with mismatch detection
|
||||
- **Automated scoring**: Overall, Technical, Content, Performance, Social scores (0-100)
|
||||
- **Issue detection**: Critical/Warning/Info severity levels across all categories
|
||||
- **Plan-First Flow Fix** — Non-code agents (SEO, content, SMM, design, web, app) now bypass the plan-first workflow
|
||||
- No more "Start Coding" or "Modify Plan" buttons for non-code agent responses
|
||||
- SEO agent goes straight to preview mode after generating audit reports
|
||||
- Plan card only shown for code/general agents
|
||||
|
||||
### Changed
|
||||
- `/api/fetch-url` route completely rewritten: 300+ lines of comprehensive HTML parsing and scoring
|
||||
- SEO agent data injection now passes structured audit data with scores, issues, and category breakdowns
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 5 (AIAssist.tsx, qwen-oauth.ts, ollama-cloud.ts, zai-plan.ts, openrouter.ts)
|
||||
- Files rewritten: 1 (`app/api/fetch-url/route.ts` — complete rewrite)
|
||||
- New component-level guard: `isCodeAgent` condition for plan-first workflow
|
||||
|
||||
## [1.6.0] - 2026-03-18 20:34 UTC
|
||||
|
||||
### Added
|
||||
- **SEO Web Audit** — SEO agent can now fetch and analyze live websites
|
||||
- `/api/fetch-url` route extracts: title, meta tags, headings (h1-h6), links (internal/external), images (alt text coverage), canonical URL, OG tags, text content length
|
||||
- Auto-detects URLs in user input when SEO agent is active
|
||||
- Pre-fetches page data before sending to AI for comprehensive audit reports
|
||||
- Supports up to 2 URLs per request
|
||||
- **SEO Auto Web Search** — When SEO agent is active and no URL is provided, web search is automatically triggered
|
||||
- Uses same SearXNG infrastructure as manual web search toggle
|
||||
- No manual toggle needed in SEO mode
|
||||
- **Updated SEO Agent Prompt** — All 4 AI services now instruct SEO agent about web audit and search capabilities
|
||||
- Added `[WEB_AUDIT:url]` and `[WEB_SEARCH:query]` tool markers
|
||||
- Added "WEB TOOLS" section to system prompts
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 5 (AIAssist.tsx, qwen-oauth.ts, ollama-cloud.ts, zai-plan.ts, openrouter.ts)
|
||||
- Files added: 1 (`app/api/fetch-url/route.ts`)
|
||||
- New API endpoint: `GET /api/fetch-url?url=https://example.com`
|
||||
|
||||
## [1.5.0] - 2026-03-18 20:29 UTC
|
||||
|
||||
### Added
|
||||
- **Modification Progress Overlay** — Visually appealing spinner when modifying existing generated code
|
||||
- Full-screen canvas overlay with spinning ring + Wrench icon
|
||||
- "Modification in Progress" label with animated bouncing dots
|
||||
- Canvas hidden during modification, revealed only when AI finishes
|
||||
- `isModifying` state tracked via `assistStep === "preview"` detection
|
||||
|
||||
### Fixed
|
||||
- **Preview blink during modification** — Old preview no longer flashes while AI rewrites code
|
||||
|
||||
### Technical Details
|
||||
- Files modified: 1 (AIAssist.tsx: +21/-2 lines)
|
||||
- New state:
|
||||
- New import: from lucide-react
|
||||
|
||||
|
||||
## [1.4.0] - 2026-03-18 19:57 UTC
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -1,5 +1,7 @@
|
||||
# PromptArch: AI Orchestration Platform
|
||||
|
||||
> **Latest Version**: [v2.2.1](CHANGELOG.md#2.2.1---2026-03-19) (2026-03-19)(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).**
|
||||
|
||||
@@ -19,7 +21,7 @@ Transform vague ideas into production-ready prompts and PRDs. PromptArch is an A
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **AI Assist** | Plan-first workflow: describe a task, get a structured plan, approve, then generate working code with live preview |
|
||||
| **Vibe Architect** | Plan-first workflow: describe a task, get a structured plan, approve, then generate working code with live preview |
|
||||
| **Prompt Enhancer** | Refine vague prompts into surgical instructions using 9 enhancement strategies and 11+ intent patterns |
|
||||
| **PRD Generator** | Convert ideas into structured Product Requirements Documents |
|
||||
| **Action Plan** | Decompose PRDs into actionable development steps and framework recommendations |
|
||||
@@ -141,6 +143,15 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
| Version | Date | Highlights |
|
||||
|---------|------|------------|
|
||||
| [2.2.0](CHANGELOG.md#2.2.0---2026-03-19) | Leads Finder Agent Mode | 2026-03-19 |
|
||||
| [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.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 |
|
||||
| [1.6.0](CHANGELOG.md#160---2026-03-18) | 2026-03-18 20:34 | SEO web audit, URL fetching, auto web search for SEO mode |
|
||||
| [1.5.0](CHANGELOG.md#150---2026-03-18) | 2026-03-18 20:29 | Modification progress overlay, preview blink fix |
|
||||
| [1.4.0](CHANGELOG.md#140---2026-03-18) | 2026-03-18 19:57 | Review Code button, web search grounding, responsive preview, model selector fix |
|
||||
| [1.3.0](CHANGELOG.md#130---2026-03-18) | 2026-03-18 18:51 | Plan-first workflow, OpenRouter, post-coding UX, enhanced prompt engine |
|
||||
| [1.2.0](CHANGELOG.md#120---2026-01-19) | 2026-01-19 19:16 | SEO agent fixes, Z.AI API validation |
|
||||
|
||||
269
app/api/fetch-url/route.ts
Normal file
269
app/api/fetch-url/route.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Next.js API route: Fetch URL content for comprehensive SEO/GEO auditing.
|
||||
* Endpoint: GET /api/fetch-url?url=https://example.com
|
||||
* Returns: Full SEO audit data (technical, content, performance signals, accessibility)
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
function countOccurrences(text: string, regex: RegExp): number {
|
||||
return (text.match(regex) || []).length;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const targetUrl = request.nextUrl.searchParams.get("url");
|
||||
|
||||
if (!targetUrl) {
|
||||
return NextResponse.json({ error: "URL parameter required" }, { status: 400 });
|
||||
}
|
||||
|
||||
let normalizedUrl = targetUrl;
|
||||
if (!normalizedUrl.startsWith("http")) normalizedUrl = "https://" + normalizedUrl;
|
||||
|
||||
try {
|
||||
new URL(normalizedUrl);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid URL" }, { status: 400 });
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 15000);
|
||||
|
||||
const res = await fetch(normalizedUrl, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (compatible; PromptArch-SEOAudit/1.6; +https://rommark.dev)",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
},
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ error: `HTTP ${res.status}`, status: res.status, url: normalizedUrl, responseTime });
|
||||
}
|
||||
|
||||
const html = await res.text();
|
||||
const urlObj = new URL(normalizedUrl);
|
||||
const baseDomain = urlObj.hostname;
|
||||
|
||||
// === HTTP HEADERS ===
|
||||
const headers: Record<string, string> = {};
|
||||
res.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; });
|
||||
const isHttps = normalizedUrl.startsWith("https://");
|
||||
const server = headers["server"] || "Unknown";
|
||||
const xFrameOptions = headers["x-frame-options"] || null;
|
||||
const contentEncoding = headers["content-encoding"] || "";
|
||||
|
||||
// === ROBOTS & CANONICAL ===
|
||||
const robotsMeta = html.match(/<meta[^>]*name\s*=\s*["']robots["'][^>]*content\s*=\s*["']([\s\S]*?)["']/i)
|
||||
|| html.match(/<meta[^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*name\s*=\s*["']robots["']/i);
|
||||
const robotsDirectives = robotsMeta ? robotsMeta[1].trim() : null;
|
||||
|
||||
const canonicalMatch = html.match(/<link[^>]*rel\s*=\s*["']canonical["'][^>]*href\s*=\s*["']([^"']*)["']/i)
|
||||
|| html.match(/<link[^>]*href\s*=\s*["']([^"']*)["'][^>]*rel\s*=\s*["']canonical["']/i);
|
||||
const canonical = canonicalMatch ? canonicalMatch[1] : null;
|
||||
const hasCanonicalMismatch = canonical && canonical !== normalizedUrl && !canonical.startsWith("/") && new URL(canonical).href !== new URL(normalizedUrl).href;
|
||||
|
||||
// === META TAGS ===
|
||||
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
||||
const title = titleMatch ? titleMatch[1].trim() : null;
|
||||
const titleLength = title ? title.length : 0;
|
||||
|
||||
const descMatch = html.match(/<meta[^>]*name\s*=\s*["']description["'][^>]*content\s*=\s*["']([\s\S]*?)["']/i)
|
||||
|| html.match(/<meta[^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*name\s*=\s*["']description["']/i);
|
||||
const metaDescription = descMatch ? descMatch[1].trim() : null;
|
||||
const descLength = metaDescription ? metaDescription.length : 0;
|
||||
|
||||
const kwMatch = html.match(/<meta[^>]*name\s*=\s*["']keywords["'][^>]*content\s*=\s*["']([\s\S]*?)["']/i)
|
||||
|| html.match(/<meta[^>]*content\s*=\s*["']([\s\S]*?)["'][^>]*name\s*=\s*["']keywords["']/i);
|
||||
const metaKeywords = kwMatch ? kwMatch[1].trim() : null;
|
||||
|
||||
const viewportMatch = html.match(/<meta[^>]*name\s*=\s*["']viewport["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
const viewport = viewportMatch ? viewportMatch[1].trim() : null;
|
||||
|
||||
const charsetMatch = html.match(/<meta[^>]*charset\s*=\s*["']?([^"'\s>]+)/i)
|
||||
|| html.match(/<meta[^>]*content\s*=\s*["'][^"']*charset=([^"'\s]+)/i);
|
||||
const charset = charsetMatch ? charsetMatch[1].trim() : null;
|
||||
|
||||
// === OPEN GRAPH ===
|
||||
const ogTitle = html.match(/<meta[^>]*property\s*=\s*["']og:title["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
const ogDesc = html.match(/<meta[^>]*property\s*=\s*["']og:description["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
const ogImage = html.match(/<meta[^>]*property\s*=\s*["']og:image["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
const ogType = html.match(/<meta[^>]*property\s*=\s*["']og:type["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
const ogUrl = html.match(/<meta[^>]*property\s*=\s*["']og:url["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
|
||||
// === TWITTER CARD ===
|
||||
const twCard = html.match(/<meta[^>]*name\s*=\s*["']twitter:card["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
const twTitle = html.match(/<meta[^>]*name\s*=\s*["']twitter:title["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
const twDesc = html.match(/<meta[^>]*name\s*=\s*["']twitter:description["'][^>]*content\s*=\s*["']([^"']*)["']/i);
|
||||
|
||||
// === HEADING STRUCTURE ===
|
||||
const headings: { level: number; text: string }[] = [];
|
||||
const headingRegex = /<h([1-6])[^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
||||
let hMatch;
|
||||
while ((hMatch = headingRegex.exec(html)) !== null) {
|
||||
const text = hMatch[2].replace(/<[^>]*>/g, "").trim();
|
||||
if (text) headings.push({ level: parseInt(hMatch[1]), text });
|
||||
}
|
||||
const h1Count = headings.filter(h => h.level === 1).length;
|
||||
const h2Count = headings.filter(h => h.level === 2).length;
|
||||
const h3Count = headings.filter(h => h.level === 3).length;
|
||||
const h4Count = headings.filter(h => h.level === 4).length;
|
||||
const headingHierarchy = headings.map(h => ({ level: h.level, text: h.text.substring(0, 100) }));
|
||||
|
||||
// === LINKS ===
|
||||
const links: { href: string; text: string; internal: boolean; nofollow: boolean }[] = [];
|
||||
const linkRegex = /<a[^>]*href\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi;
|
||||
let lMatch;
|
||||
while ((lMatch = linkRegex.exec(html)) !== null) {
|
||||
const href = lMatch[1].trim();
|
||||
const fullTag = lMatch[0];
|
||||
const text = lMatch[2].replace(/<[^>]*>/g, "").trim().substring(0, 100);
|
||||
if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:")) continue;
|
||||
const isNofollow = /rel\s*=\s*["'][^"']*nofollow/i.test(fullTag);
|
||||
try {
|
||||
const linkDomain = new URL(href, normalizedUrl).hostname;
|
||||
links.push({ href, text, internal: linkDomain === baseDomain, nofollow: isNofollow });
|
||||
} catch { continue; }
|
||||
}
|
||||
const internalLinks = links.filter(l => l.internal);
|
||||
const externalLinks = links.filter(l => !l.internal);
|
||||
const nofollowLinks = links.filter(l => l.nofollow);
|
||||
|
||||
// === IMAGES ===
|
||||
const images: { src: string; alt: string; loading?: string }[] = [];
|
||||
const imgRegex = /<img([^>]*)\/?>/gi;
|
||||
let iMatch;
|
||||
while ((iMatch = imgRegex.exec(html)) !== null) {
|
||||
const attrs = iMatch[1];
|
||||
const srcMatch = attrs.match(/src\s*=\s*["']([^"']*)["']/i);
|
||||
const altMatch = attrs.match(/alt\s*=\s*["']([^"']*)["']/i);
|
||||
const loadMatch = attrs.match(/loading\s*=\s*["']([^"']*)["']/i);
|
||||
if (srcMatch) {
|
||||
images.push({ src: srcMatch[1], alt: altMatch ? altMatch[1] : "", loading: loadMatch ? loadMatch[1] : undefined });
|
||||
}
|
||||
}
|
||||
const imagesWithAlt = images.filter(img => img.alt && img.alt.trim().length > 0);
|
||||
const imagesWithoutAlt = images.filter(img => !img.alt || !img.alt.trim());
|
||||
const lazyLoadedImages = images.filter(img => img.loading === "lazy");
|
||||
|
||||
// === CONTENT ANALYSIS ===
|
||||
const plainText = html
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
||||
.replace(/<noscript[\s\S]*?<\/noscript>/gi, "")
|
||||
.replace(/<[^>]*>/g, " ")
|
||||
.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<")
|
||||
.replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'")
|
||||
.replace(/\s+/g, " ").trim();
|
||||
|
||||
const wordCount = plainText ? plainText.split(/\s+/).length : 0;
|
||||
const sentenceCount = plainText ? (plainText.match(/[.!?]+/g) || []).length : 0;
|
||||
const paragraphCount = plainText ? (plainText.match(/\n\s*\n/g) || []).length : 0;
|
||||
const avgWordsPerSentence = sentenceCount > 0 ? Math.round(wordCount / sentenceCount) : 0;
|
||||
|
||||
// === STRUCTURED DATA ===
|
||||
const sdTypes = ["Article", "BlogPosting", "FAQPage", "HowTo", "Product", "LocalBusiness", "Organization", "BreadcrumbList", "WebSite", "SearchAction", "VideoObject", "Review"];
|
||||
const structuredData = sdTypes.map(sdType => ({
|
||||
type: sdType,
|
||||
found: new RegExp('"@type"\\s*:\\s*"' + sdType + '"', "i").test(html)
|
||||
|| new RegExp('"@type"\\s*:\\s*\\["' + sdType + '"', "i").test(html),
|
||||
}));
|
||||
const hasJsonLd = /<script[^>]*type\s*=\s*["']application\/ld\+json["']/i.test(html);
|
||||
const hasMicrodata = /itemscope/i.test(html);
|
||||
|
||||
// === HREFLANG ===
|
||||
const hreflangTags: { lang: string; href: string }[] = [];
|
||||
const hlRegex = /<link[^>]*rel\s*=\s*["']alternate["'][^>]*hreflang\s*=\s*["']([^"']*)["'][^>]*href\s*=\s*["']([^"']*)["']/i;
|
||||
let hlMatch;
|
||||
while ((hlMatch = hlRegex.exec(html)) !== null) {
|
||||
hreflangTags.push({ lang: hlMatch[1], href: hlMatch[2] });
|
||||
}
|
||||
|
||||
// === PERFORMANCE SIGNALS ===
|
||||
const htmlSize = html.length;
|
||||
const inlineStyleCount = countOccurrences(html, /style\s*=\s*"/g);
|
||||
const inlineScriptCount = countOccurrences(html, /<script(?!.*src)/gi);
|
||||
const externalScripts = countOccurrences(html, /<script[^>]*src\s*=/gi);
|
||||
const externalStylesheets = countOccurrences(html, /<link[^>]*stylesheet/gi);
|
||||
const hasPreconnect = /<link[^>]*rel\s*=\s*["']preconnect["']/i.test(html);
|
||||
const hasPreload = /<link[^>]*rel\s*=\s*["']preload["']/i.test(html);
|
||||
const hasDnsPrefetch = /<link[^>]*rel\s*=\s*["']dns-prefetch["']/i.test(html);
|
||||
const usesAsyncScripts = /async\s*=/.test(html);
|
||||
const usesDeferScripts = /defer\s*=/.test(html);
|
||||
|
||||
// === ACCESSIBILITY ===
|
||||
const hasLangAttr = /<html[^>]*lang\s*=/i.test(html);
|
||||
const hasAriaLabels = /aria-label|aria-labelledby|aria-describedby/i.test(html);
|
||||
|
||||
// === SCORE CALCULATION ===
|
||||
let score = 100;
|
||||
const issues: { severity: "critical" | "warning" | "info"; category: string; message: string }[] = [];
|
||||
|
||||
if (!title) { score -= 10; issues.push({ severity: "critical", category: "Meta", message: "Missing title tag" }); }
|
||||
else if (titleLength > 60) { score -= 3; issues.push({ severity: "warning", category: "Meta", message: "Title too long (" + titleLength + " chars, max 60)" }); }
|
||||
if (!metaDescription) { score -= 10; issues.push({ severity: "critical", category: "Meta", message: "Missing meta description" }); }
|
||||
else if (descLength > 160) { score -= 3; issues.push({ severity: "warning", category: "Meta", message: "Meta description too long (" + descLength + " chars, max 160)" }); }
|
||||
if (h1Count === 0) { score -= 10; issues.push({ severity: "critical", category: "Content", message: "Missing H1 heading" }); }
|
||||
if (h1Count > 1) { score -= 5; issues.push({ severity: "critical", category: "Content", message: "Multiple H1 tags (" + h1Count + " found)" }); }
|
||||
if (!viewport) { score -= 10; issues.push({ severity: "critical", category: "Mobile", message: "Missing viewport meta tag" }); }
|
||||
if (!isHttps) { score -= 10; issues.push({ severity: "critical", category: "Security", message: "Not using HTTPS" }); }
|
||||
if (imagesWithoutAlt.length > 0) { score -= 5; issues.push({ severity: "warning", category: "Accessibility", message: imagesWithoutAlt.length + " images missing alt text" }); }
|
||||
if (!canonical) { score -= 3; issues.push({ severity: "warning", category: "Technical", message: "Missing canonical tag" }); }
|
||||
if (hasCanonicalMismatch) { score -= 5; issues.push({ severity: "warning", category: "Technical", message: "Canonical URL mismatch" }); }
|
||||
if (inlineStyleCount > 10) { score -= 3; issues.push({ severity: "warning", category: "Performance", message: inlineStyleCount + " inline styles detected" }); }
|
||||
if (!hasPreconnect && externalScripts > 3) { score -= 3; issues.push({ severity: "warning", category: "Performance", message: "Missing preconnect hints for external resources" }); }
|
||||
if (wordCount < 300 && wordCount > 0) { score -= 3; issues.push({ severity: "warning", category: "Content", message: "Thin content (" + wordCount + " words)" }); }
|
||||
if (!ogTitle && !ogDesc) { score -= 3; issues.push({ severity: "warning", category: "Social", message: "Missing Open Graph tags" }); }
|
||||
if (!twCard) { score -= 2; issues.push({ severity: "warning", category: "Social", message: "Missing Twitter Card tags" }); }
|
||||
if (externalLinks.length === 0) { score -= 2; issues.push({ severity: "warning", category: "Links", message: "No external links found" }); }
|
||||
if (robotsDirectives && /noindex/i.test(robotsDirectives)) { score -= 10; issues.push({ severity: "critical", category: "Technical", message: "Page has noindex directive" }); }
|
||||
if (!hasJsonLd && !hasMicrodata) { score -= 1; issues.push({ severity: "info", category: "Structured Data", message: "No structured data found" }); }
|
||||
if (!hasLangAttr) { score -= 1; issues.push({ severity: "info", category: "Accessibility", message: "Missing html lang attribute" }); }
|
||||
if (!lazyLoadedImages.length && images.length > 5) { score -= 2; issues.push({ severity: "info", category: "Performance", message: "Consider lazy loading for images" }); }
|
||||
|
||||
score = Math.max(0, Math.min(100, score));
|
||||
|
||||
const technicalScore = Math.min(100, 100 - issues.filter(i => i.category === "Technical" || i.category === "Security").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
|
||||
const contentScore = Math.min(100, 100 - issues.filter(i => i.category === "Content").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
|
||||
const performanceScore = Math.min(100, 100 - issues.filter(i => i.category === "Performance" || i.category === "Mobile").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
|
||||
const socialScore = Math.min(100, 100 - issues.filter(i => i.category === "Social" || i.category === "Structured Data").reduce((s, i) => s + (i.severity === "critical" ? 15 : i.severity === "warning" ? 7 : 2), 0));
|
||||
|
||||
return NextResponse.json({
|
||||
url: normalizedUrl,
|
||||
domain: baseDomain,
|
||||
protocol: isHttps ? "HTTPS" : "HTTP",
|
||||
responseTime,
|
||||
server,
|
||||
htmlSize,
|
||||
title, titleLength,
|
||||
titleStatus: !title ? "missing" : titleLength > 60 ? "too_long" : "good",
|
||||
metaDescription, descLength,
|
||||
descStatus: !metaDescription ? "missing" : descLength > 160 ? "too_long" : "good",
|
||||
metaKeywords, viewport, charset, robotsDirectives,
|
||||
canonical, hasCanonicalMismatch, xFrameOptions,
|
||||
openGraph: { title: ogTitle ? ogTitle[1] : null, description: ogDesc ? ogDesc[1] : null, image: ogImage ? ogImage[1] : null, type: ogType ? ogType[1] : null, url: ogUrl ? ogUrl[1] : null },
|
||||
twitterCard: { card: twCard ? twCard[1] : null, title: twTitle ? twTitle[1] : null, description: twDesc ? twDesc[1] : null },
|
||||
headings: headingHierarchy, h1Count, h2Count, h3Count, h4Count,
|
||||
headingStatus: h1Count === 0 ? "missing_h1" : h1Count > 1 ? "multiple_h1" : "good",
|
||||
links: { total: links.length, internal: internalLinks.length, external: externalLinks.length, nofollow: nofollowLinks.length, sampleExternal: externalLinks.slice(0, 20).map(l => ({ href: l.href, text: l.text, nofollow: l.nofollow })) },
|
||||
images: { total: images.length, withAlt: imagesWithAlt.length, withoutAlt: imagesWithoutAlt.length, lazyLoaded: lazyLoadedImages.length, altCoverage: images.length > 0 ? Math.round((imagesWithAlt.length / images.length) * 100) : 100, sampleWithoutAlt: imagesWithoutAlt.slice(0, 10).map(img => img.src) },
|
||||
content: { wordCount, sentenceCount, paragraphCount, avgWordsPerSentence, textPreview: plainText.substring(0, 2000) },
|
||||
structuredData: { hasJsonLd, hasMicrodata, types: structuredData },
|
||||
hreflang: hreflangTags,
|
||||
performance: { inlineStyles: inlineStyleCount, inlineScripts: inlineScriptCount, externalScripts, externalStylesheets, hasPreconnect, hasPreload, hasDnsPrefetch, usesAsyncScripts, usesDeferScripts, contentEncoding },
|
||||
accessibility: { hasLangAttr, hasAriaLabels, hasAltOnFirstImage: images.length > 0 && images[0].alt && images[0].alt.trim().length > 0 },
|
||||
scores: { overall: score, technical: technicalScore, content: contentScore, performance: performanceScore, social: socialScore },
|
||||
issues,
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : "Fetch failed";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -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...");
|
||||
|
||||
20
app/vibe/page.tsx
Normal file
20
app/vibe/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import dynamic from 'next/dynamic';
|
||||
import modelAdapter from "@/lib/services/adapter-instance";
|
||||
|
||||
const AIAssist = dynamic(() => import("@/components/AIAssist"), { ssr: false });
|
||||
|
||||
export default function VibePage() {
|
||||
useEffect(() => {
|
||||
console.log("[Vibe] Initializing services...");
|
||||
modelAdapter["qwenService"]["initialize"]?.();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-[#050508] overflow-hidden">
|
||||
<AIAssist vibeMode />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
Wand2, LayoutPanelLeft, Play, Orbit, Plus, Key, ShieldCheck, Wrench, FileText, Users
|
||||
} from "lucide-react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@@ -361,7 +362,7 @@ function parseStreamingContent(text: string, currentAgent: string) {
|
||||
// 3. Clean display text - hide all tag-like sequences and their partials
|
||||
chatDisplay = text
|
||||
// Hide complete tags (flexible brackets), including SUGGEST_AGENT
|
||||
.replace(/\[+(?:AGENT|SUGGEST_AGENT|content|seo|smm|pm|code|design|web|app|PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT|PREV):?[\w-]*:?[\w-]*\]+/gi, "")
|
||||
.replace(/\[+(?:AGENT|SUGGEST_AGENT|content|seo|smm|pm|code|design|web|app|PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT|PREV|WEB_AUDIT|WEB_SEARCH):?[\w-]*:?https?:\/\/[^\]]*\]+/gi, "").replace(/\[+(?:AGENT|SUGGEST_AGENT|content|seo|smm|pm|code|design|web|app|PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT|PREV|WEB_AUDIT|WEB_SEARCH):?[\w-]*:?[\w-]*\]+/gi, "")
|
||||
// Hide content inside preview block (cleanly)
|
||||
.replace(/\[+PREVIEW:[\w-]+:?[\w-]+?\]+[\s\S]*?(?:\[\/(?:PREVIEW|APP|WEB|SEO|CODE|DESIGN|SMM|PM|CONTENT)\]+|$)/gi, "")
|
||||
// Hide closing tags
|
||||
@@ -477,7 +478,7 @@ function parsePlanFromResponse(text: string): { plan: Record<string, any> | null
|
||||
|
||||
// --- Main Component ---
|
||||
|
||||
export default function AIAssist() {
|
||||
export default function AIAssist({ vibeMode = false }: { vibeMode?: boolean } = {}) {
|
||||
const {
|
||||
language,
|
||||
aiAssistTabs,
|
||||
@@ -496,6 +497,8 @@ export default function AIAssist() {
|
||||
} = useStore();
|
||||
const t = translations[language].aiAssist;
|
||||
const common = translations[language].common;
|
||||
// Vibe mode: override labels
|
||||
const _vibe = vibeMode ? { studioTitle: "Vibe Architect", studioDesc: "Describe your vision. Get plans, code, and live previews." } : {};
|
||||
|
||||
const activeTab = aiAssistTabs?.find(tab => tab.id === activeTabId) || aiAssistTabs?.[0] || {
|
||||
id: 'default',
|
||||
@@ -517,6 +520,8 @@ export default function AIAssist() {
|
||||
const [viewMode, setViewMode] = useState<"preview" | "code">("preview");
|
||||
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
|
||||
@@ -574,7 +579,7 @@ export default function AIAssist() {
|
||||
/module\.exports/i.test(preview.data);
|
||||
|
||||
// Client-side detection
|
||||
const isUI = ["web", "app", "design", "html", "ui"].includes(preview.type);
|
||||
const isUI = ["web", "app", "design", "html", "ui", "leads"].includes(preview.type);
|
||||
const hasTags = /<[a-z][\s\S]*>/i.test(preview.data);
|
||||
|
||||
return (isUI || hasTags || preview.language === "html") && !isBackend;
|
||||
@@ -637,10 +642,15 @@ export default function AIAssist() {
|
||||
setInput("");
|
||||
}
|
||||
|
||||
// Capture whether this is the initial plan phase (before any code generation)
|
||||
const wasIdle = !isApproval && (assistStep === "idle" || assistStep === "plan");
|
||||
// Plan-first workflow only applies to the "code" agent (software architect)
|
||||
// SEO, content, smm, design, web, app agents go straight to preview
|
||||
const isCodeAgent = currentAgent === "code";
|
||||
const wasIdle = !isApproval && isCodeAgent && (assistStep === "idle" || assistStep === "plan");
|
||||
// Detect if user is modifying an existing visual artifact (not text-only agents like SEO/content)
|
||||
const isVisualAgent = currentAgent === "code" || currentAgent === "web" || currentAgent === "app" || currentAgent === "design" || currentAgent === "general" || currentAgent === "leads";
|
||||
if (assistStep === "preview" && isVisualAgent) setIsModifying(true);
|
||||
setIsProcessing(true);
|
||||
if (assistStep === "idle") setAssistStep("plan");
|
||||
if (assistStep === "idle" && isCodeAgent) setAssistStep("plan");
|
||||
|
||||
const assistantMsg: AIAssistMessage = {
|
||||
role: "assistant",
|
||||
@@ -692,6 +702,120 @@ export default function AIAssist() {
|
||||
setStatus(null);
|
||||
}
|
||||
|
||||
// SEO mode: auto-fetch URLs from user input for comprehensive live auditing
|
||||
if (currentAgent === "seo") {
|
||||
const urlMatches = [...finalInput.matchAll(/https?:\/\/[^\s<>"')\]]+/gi)].map(m => m[0]);
|
||||
const uniqueUrls = [...new Set(urlMatches)].slice(0, 2);
|
||||
if (uniqueUrls.length > 0) {
|
||||
setStatus("Auditing website" + (uniqueUrls.length > 1 ? "s" : "") + "...");
|
||||
try {
|
||||
for (const url of uniqueUrls) {
|
||||
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";
|
||||
enrichedInput += "== META TAGS ==\n";
|
||||
enrichedInput += "Title: " + (d.title || "MISSING") + " (" + (d.titleLength || 0) + " chars) [" + (d.titleStatus || "?") + "]\n";
|
||||
enrichedInput += "Description: " + (d.metaDescription || "MISSING") + " (" + (d.descLength || 0) + " chars) [" + (d.descStatus || "?") + "]\n";
|
||||
enrichedInput += "Keywords: " + (d.metaKeywords || "None") + "\n";
|
||||
enrichedInput += "Viewport: " + (d.viewport || "MISSING") + " | Charset: " + (d.charset || "None") + "\n";
|
||||
enrichedInput += "Canonical: " + (d.canonical || "None") + (d.hasCanonicalMismatch ? " [MISMATCH!]" : "") + "\n";
|
||||
enrichedInput += "Robots: " + (d.robotsDirectives || "None") + "\n\n";
|
||||
enrichedInput += "== OPEN GRAPH & SOCIAL ==\n";
|
||||
enrichedInput += "OG Title: " + (d.openGraph?.title || "None") + " | OG Desc: " + (d.openGraph?.description || "None") + " | OG Image: " + (d.openGraph?.image || "None") + "\n";
|
||||
enrichedInput += "Twitter Card: " + (d.twitterCard?.card || "None") + "\n\n";
|
||||
enrichedInput += "== HEADINGS ==\n";
|
||||
enrichedInput += "H1 count: " + d.h1Count + " (should be 1) | H2: " + d.h2Count + " | H3: " + d.h3Count + " [" + (d.headingStatus || "?") + "]\n";
|
||||
if (d.headings && d.headings.length > 0) {
|
||||
for (const h of d.headings.slice(0, 15)) enrichedInput += " H" + h.level + ": " + h.text + "\n";
|
||||
}
|
||||
enrichedInput += "\n== LINKS ==\n";
|
||||
const lnks = d.links || {};
|
||||
enrichedInput += "Total: " + (lnks.total || 0) + " | Internal: " + (lnks.internal || 0) + " | External: " + (lnks.external || 0) + " | Nofollow: " + (lnks.nofollow || 0) + "\n";
|
||||
if (lnks.sampleExternal && lnks.sampleExternal.length > 0) {
|
||||
enrichedInput += "Sample external: " + lnks.sampleExternal.slice(0, 10).map((l: { href: string }) => l.href).join(", ") + "\n";
|
||||
}
|
||||
enrichedInput += "\n== IMAGES ==\n";
|
||||
const imgs = d.images || {};
|
||||
enrichedInput += "Total: " + (imgs.total || 0) + " | With alt: " + (imgs.withAlt || 0) + " | Without alt: " + (imgs.withoutAlt || 0) + " | Lazy loaded: " + (imgs.lazyLoaded || 0) + "\n";
|
||||
enrichedInput += "Alt text coverage: " + (imgs.altCoverage || 0) + "%\n";
|
||||
if (imgs.sampleWithoutAlt && imgs.sampleWithoutAlt.length > 0) {
|
||||
enrichedInput += "Missing alt: " + imgs.sampleWithoutAlt.slice(0, 5).join(", ") + "\n";
|
||||
}
|
||||
enrichedInput += "\n== CONTENT ==\n";
|
||||
const cnt = d.content || {};
|
||||
enrichedInput += "Words: " + (cnt.wordCount || 0) + " | Sentences: " + (cnt.sentenceCount || 0) + " | Paragraphs: " + (cnt.paragraphCount || 0) + " | Avg words/sentence: " + (cnt.avgWordsPerSentence || 0) + "\n\n";
|
||||
enrichedInput += "== STRUCTURED DATA ==\n";
|
||||
enrichedInput += "JSON-LD: " + (d.structuredData?.hasJsonLd ? "Yes" : "No") + " | Microdata: " + (d.structuredData?.hasMicrodata ? "Yes" : "No") + "\n";
|
||||
const foundTypes = (d.structuredData?.types || []).filter((t: { found: boolean }) => t.found).map((t: { type: string }) => t.type);
|
||||
enrichedInput += "Schema types found: " + (foundTypes.length > 0 ? foundTypes.join(", ") : "None") + "\n\n";
|
||||
enrichedInput += "== PERFORMANCE SIGNALS ==\n";
|
||||
const perf = d.performance || {};
|
||||
enrichedInput += "Server: " + (d.server || "Unknown") + " | Response time: " + d.responseTime + "ms | HTML size: " + d.htmlSize + " bytes\n";
|
||||
enrichedInput += "External scripts: " + (perf.externalScripts || 0) + " | External stylesheets: " + (perf.externalStylesheets || 0) + " | Inline styles: " + (perf.inlineStyles || 0) + "\n";
|
||||
enrichedInput += "Preconnect: " + (perf.hasPreconnect ? "Yes" : "No") + " | Preload: " + (perf.hasPreload ? "Yes" : "No") + " | DNS prefetch: " + (perf.hasDnsPrefetch ? "Yes" : "No") + "\n";
|
||||
enrichedInput += "Async scripts: " + (perf.usesAsyncScripts ? "Yes" : "No") + " | Defer scripts: " + (perf.usesDeferScripts ? "Yes" : "No") + "\n\n";
|
||||
enrichedInput += "== ACCESSIBILITY ==\n";
|
||||
const acc = d.accessibility || {};
|
||||
enrichedInput += "Lang attr: " + (acc.hasLangAttr ? "Yes" : "No") + " | ARIA labels: " + (acc.hasAriaLabels ? "Yes" : "No") + " | First image alt: " + (acc.hasAltOnFirstImage ? "Yes" : "No") + "\n";
|
||||
if (d.hreflang && d.hreflang.length > 0) {
|
||||
enrichedInput += "\n== HREFLANG ==\n" + d.hreflang.map((h: { lang: string; href: string }) => h.lang + " -> " + h.href).join("\n") + "\n";
|
||||
}
|
||||
enrichedInput += "\n== ISSUES FOUND ==\n";
|
||||
if (d.issues && d.issues.length > 0) {
|
||||
for (const issue of d.issues) {
|
||||
enrichedInput += "[" + issue.severity.toUpperCase() + "] " + issue.category + ": " + issue.message + "\n";
|
||||
}
|
||||
} else {
|
||||
enrichedInput += "No issues detected.\n";
|
||||
}
|
||||
enrichedInput += "[/COMPREHENSIVE SEO AUDIT]\n";
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn("Website audit failed:", e); }
|
||||
setStatus(null);
|
||||
}
|
||||
|
||||
// If no URL found and web search not enabled, auto-enable web search for SEO
|
||||
if (uniqueUrls.length === 0 && !webSearchEnabled) {
|
||||
try {
|
||||
setStatus("Searching for SEO context...");
|
||||
const searchRes = await fetch("/api/search?q=" + encodeURIComponent(finalInput.split("\n")[0].substring(0, 200)));
|
||||
if (searchRes.ok) {
|
||||
const searchData = await searchRes.json();
|
||||
if (searchData.results && searchData.results.length > 0) {
|
||||
const searchContext = searchData.results.slice(0, 5).map((r: { title: string; url: string; snippet: string }, i: number) =>
|
||||
(i + 1) + ". **" + r.title + "** (" + r.url + ") - " + r.snippet
|
||||
).join("\n");
|
||||
enrichedInput = "[WEB SEARCH CONTEXT - Top 5 relevant results]\n" + searchContext + "\n\n---\nUsing the above search results as reference context, answer the user query. Cite sources when relevant.\n\nUser query: " + finalInput;
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn("SEO web search failed:", e); }
|
||||
setStatus(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Leads mode: auto-search for leads
|
||||
if (currentAgent === "leads" && !webSearchEnabled) {
|
||||
try {
|
||||
setStatus("Finding leads...");
|
||||
const searchRes = await fetch("/api/search?q=" + encodeURIComponent(finalInput.split("\n")[0].substring(0, 200)));
|
||||
if (searchRes.ok) {
|
||||
const searchData = await searchRes.json();
|
||||
if (searchData.results && searchData.results.length > 0) {
|
||||
const searchContext = searchData.results.slice(0, 5).map((r: { title: string; url: string; snippet: string }, i: number) =>
|
||||
(i + 1) + ". **" + r.title + "** (" + r.url + ") - " + r.snippet
|
||||
).join("\n");
|
||||
enrichedInput = "[WEB SEARCH CONTEXT - Use these results to find leads]\n" + searchContext + "\n\n---\nExtract leads/prospects from the above results. Search for more leads using [WEB_SEARCH:query] with different angles. Then output results as a [PREVIEW:leads:html] table.\n\nUser request: " + finalInput;
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn("Leads web search failed:", e); }
|
||||
setStatus(null);
|
||||
}
|
||||
|
||||
const response = await modelAdapter.generateAIAssistStream(
|
||||
{
|
||||
messages: [...formattedHistory, { role: "user" as const, content: enrichedInput, timestamp: new Date() }],
|
||||
@@ -750,8 +874,9 @@ export default function AIAssist() {
|
||||
|
||||
if (!response.success) throw new Error(response.error);
|
||||
|
||||
// When this was the initial request from idle/plan, ALWAYS show plan card
|
||||
// Post-stream routing
|
||||
if (wasIdle) {
|
||||
// Code agent: show plan card for approval
|
||||
setAssistStep("plan");
|
||||
const { plan: parsedPlan } = parsePlanFromResponse(accumulated);
|
||||
if (parsedPlan) {
|
||||
@@ -760,21 +885,34 @@ export default function AIAssist() {
|
||||
setAiPlan({ rawText: accumulated, architecture: "", techStack: [], files: [], steps: [] });
|
||||
}
|
||||
} else if ((lastParsedPreview as PreviewData | null)?.data) {
|
||||
// After approval: show the generated preview
|
||||
// Non-code agents or approved code: show the generated preview directly
|
||||
setAssistStep("preview");
|
||||
setShowCanvas(true);
|
||||
if (isPreviewRenderable(lastParsedPreview)) setViewMode("preview");
|
||||
} else if (!isCodeAgent && !wasIdle) {
|
||||
// Non-code agent follow-up without new preview:
|
||||
// Keep existing canvas if we had one, otherwise go idle
|
||||
if (previewData?.data) {
|
||||
setAssistStep("preview");
|
||||
} else {
|
||||
setAssistStep("idle");
|
||||
}
|
||||
} else {
|
||||
setAssistStep("idle");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Assist error:", error);
|
||||
const message = error instanceof Error ? error.message : "AI Assist failed";
|
||||
const rawMessage = error instanceof Error ? error.message : "Vibe Architect failed";
|
||||
let message = rawMessage;
|
||||
if (rawMessage.includes("429") || rawMessage.includes("insufficient_quota") || rawMessage.includes("Free allocated quota exceeded") || rawMessage.includes("rate_limit")) {
|
||||
message = "API quota exceeded or rate limited. Please switch to a different AI provider in Settings, or wait a few minutes and try again.";
|
||||
}
|
||||
const errorMsg: AIAssistMessage = { role: "assistant", content: message, timestamp: new Date() };
|
||||
updateTabById(requestTabId, { history: [...aiAssistHistory, errorMsg] });
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
setIsModifying(false);
|
||||
setAbortController(null);
|
||||
setStatus(null);
|
||||
}
|
||||
@@ -798,6 +936,55 @@ export default function AIAssist() {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
const exportSeoReport = (format: "html" | "pdf") => {
|
||||
if (!seoAuditData) return;
|
||||
downloadSeoReport(seoAuditData, format);
|
||||
};
|
||||
|
||||
const exportLeadsReport = (format: "html" | "csv") => {
|
||||
const htmlContent = previewData?.data;
|
||||
if (!htmlContent) return;
|
||||
if (format === "html") {
|
||||
const blob = new Blob([htmlContent], { type: "text/html" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "leads-report.html";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
} else {
|
||||
// Extract table rows from HTML and convert to CSV
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlContent, "text/html");
|
||||
const rows = doc.querySelectorAll("tbody tr");
|
||||
if (rows.length === 0) return;
|
||||
let csv = "Name,Platform,Followers,Region,Bio,Link\n";
|
||||
rows.forEach((row) => {
|
||||
const cells = row.querySelectorAll("td");
|
||||
if (cells.length >= 7) {
|
||||
const name = cells[1]?.textContent?.trim().replace(/,/g, ";") || "";
|
||||
const platform = cells[2]?.textContent?.trim() || "";
|
||||
const followers = cells[3]?.textContent?.trim() || "";
|
||||
const region = cells[4]?.textContent?.trim().replace(/,/g, ";") || "";
|
||||
const bio = cells[5]?.textContent?.trim().replace(/,/g, ";") || "";
|
||||
const link = cells[6]?.querySelector("a")?.href || "";
|
||||
csv += `"${name}","${platform}","${followers}","${region}","${bio}","${link}"\n`;
|
||||
}
|
||||
});
|
||||
const blob = new Blob([csv], { type: "text/csv" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "leads-report.csv";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const clearHistory = () => {
|
||||
updateActiveTab({
|
||||
@@ -989,6 +1176,7 @@ export default function AIAssist() {
|
||||
{ label: t.agents.seo, agent: "seo", icon: <Search className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.web, agent: "web", icon: <LayoutPanelLeft className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.app, agent: "app", icon: <Play className="h-3.5 w-3.5" /> },
|
||||
{ label: t.agents.leads, agent: "leads", icon: <Users className="h-3.5 w-3.5" /> },
|
||||
].map(({ label, agent, icon }) => (
|
||||
<button
|
||||
key={agent}
|
||||
@@ -1054,7 +1242,7 @@ export default function AIAssist() {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-black text-amber-600 dark:text-amber-400">Qwen Authentication Required</h4>
|
||||
<p className="text-xs text-amber-700/70 dark:text-amber-300/70 mt-0.5">Sign in with Qwen to use AI Assist with this provider</p>
|
||||
<p className="text-xs text-amber-700/70 dark:text-amber-300/70 mt-0.5">Sign in with Qwen to use Vibe Architect with this provider</p>
|
||||
{qwenAuthError && (
|
||||
<p className="text-xs text-red-500 mt-1">{qwenAuthError}</p>
|
||||
)}
|
||||
@@ -1070,7 +1258,42 @@ export default function AIAssist() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{aiAssistHistory.length === 0 && (
|
||||
{aiAssistHistory.length === 0 && currentAgent === "leads" && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center py-12 animate-in zoom-in-95 duration-500">
|
||||
<div className="p-6 bg-emerald-500/10 rounded-full mb-6 relative">
|
||||
<Users className="h-16 w-16 text-emerald-400/60" />
|
||||
<div className="absolute inset-0 bg-emerald-500/10 blur-3xl rounded-full" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-black text-slate-900 dark:text-emerald-50 mb-2 tracking-tighter">Leads Finder</h3>
|
||||
<p className="max-w-md text-sm font-medium text-slate-500 dark:text-slate-400 leading-relaxed mb-6">
|
||||
Find influencers, prospects, and leads across social media platforms. Tell me who you are looking for and I will search the web for relevant leads.
|
||||
</p>
|
||||
<div className="max-w-sm w-full bg-[#0b1414]/60 dark:bg-slate-800/50 rounded-2xl p-5 border border-emerald-500/20 mb-6 text-left">
|
||||
<p className="text-[10px] font-black text-emerald-400 uppercase tracking-widest mb-3">To get started, answer these questions:</p>
|
||||
<div className="space-y-2 text-[13px] text-slate-400 dark:text-slate-300">
|
||||
<p>1. What <span className="text-emerald-300 font-bold">niche/industry</span> are you targeting? (e.g. forex, SaaS, fitness, crypto)</p>
|
||||
<p>2. Which <span className="text-emerald-300 font-bold">region or location</span>? (e.g. Singapore, UAE, Global, Latin America)</p>
|
||||
<p>3. What <span className="text-emerald-300 font-bold">platform</span> preference? (e.g. Instagram, Twitter/X, LinkedIn, YouTube, TikTok, or all)</p>
|
||||
<p>4. How many leads do you need? (default: 20+)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-center gap-2">
|
||||
{["Forex traders in Singapore", "SaaS founders in UAE", "Fitness influencers on Instagram", "Crypto YouTubers in Latin America", "Marketing agencies in Southeast Asia"].map((label: string) => (
|
||||
<div
|
||||
key={label}
|
||||
className="px-3 py-1.5 rounded-full cursor-pointer hover:bg-emerald-600 hover:text-white transition-all text-[10px] font-bold border border-black/20 text-black bg-white/90 shadow-sm"
|
||||
onClick={() => setInput(label)}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{aiAssistHistory.length === 0 && currentAgent !== "leads" && (
|
||||
<div className="h-full flex flex-col items-center justify-center text-center py-20 animate-in zoom-in-95 duration-500">
|
||||
<div className="p-8 bg-blue-500/5 dark:bg-blue-500/10 rounded-full mb-8 relative">
|
||||
<Ghost className="h-20 w-20 text-blue-400/40 animate-bounce duration-[3s]" />
|
||||
@@ -1082,9 +1305,8 @@ export default function AIAssist() {
|
||||
</p>
|
||||
<div className="mt-10 flex flex-wrap justify-center gap-3">
|
||||
{t.suggestions.map((chip: any) => (
|
||||
<Badge
|
||||
<div
|
||||
key={chip.label}
|
||||
variant="secondary"
|
||||
className="px-4 py-2 rounded-full cursor-pointer hover:bg-blue-600 hover:text-white transition-all text-[11px] font-black border-transparent shadow-sm"
|
||||
onClick={() => {
|
||||
setCurrentAgent(chip.agent);
|
||||
@@ -1092,7 +1314,7 @@ export default function AIAssist() {
|
||||
}}
|
||||
>
|
||||
{chip.label}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1148,7 +1370,7 @@ export default function AIAssist() {
|
||||
</div>
|
||||
|
||||
{/* Agentic Plan Review Card */}
|
||||
{msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && (
|
||||
{msg.role === "assistant" && aiPlan && i === aiAssistHistory.length - 1 && assistStep === "plan" && (currentAgent === "code") && (
|
||||
<div className="mt-6 p-6 rounded-2xl bg-blue-500/5 border border-blue-500/20 backdrop-blur-sm animate-in zoom-in-95 duration-300">
|
||||
<h3 className="text-sm font-black text-blue-400 uppercase tracking-widest mb-4 flex items-center gap-2">
|
||||
<LayoutPanelLeft className="h-4 w-4" /> {t.proposedPlan}
|
||||
@@ -1169,7 +1391,7 @@ export default function AIAssist() {
|
||||
<p className="text-[11px] font-bold text-slate-500 uppercase mb-1">{t.techStack}</p>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{aiPlan.techStack?.map((t_stack: string) => (
|
||||
<Badge key={t_stack} variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t_stack}</Badge>
|
||||
<Badge variant="outline" className="text-[9px] border-blue-500/30 text-blue-300 px-1.5 py-0">{t_stack}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1209,7 +1431,6 @@ export default function AIAssist() {
|
||||
|
||||
{msg.role === "assistant" && msg.preview && !(assistStep === "preview" && i === aiAssistHistory.length - 1 && !isProcessing) && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="mt-5 w-full bg-blue-50 dark:bg-blue-900/30 border border-blue-200/60 dark:border-blue-800 text-blue-700 dark:text-blue-200 font-black uppercase tracking-[0.1em] text-[10px] rounded-2xl h-11 hover:scale-[1.02] active:scale-[0.98] transition-all"
|
||||
onClick={() => {
|
||||
@@ -1225,6 +1446,7 @@ export default function AIAssist() {
|
||||
|
||||
{/* 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"); }}
|
||||
@@ -1248,6 +1470,81 @@ export default function AIAssist() {
|
||||
<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>
|
||||
)}
|
||||
{currentAgent === "leads" && previewData?.data && (
|
||||
<div className="mt-2 flex gap-2 animate-in zoom-in-95 duration-300">
|
||||
<Button
|
||||
onClick={() => exportLeadsReport("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={() => exportLeadsReport("csv")}
|
||||
variant="outline"
|
||||
className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/20 text-blue-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 CSV</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Inline SEO Export - always visible in chat when SEO data exists */}
|
||||
{msg.role === "assistant" && msg.agent === "seo" && seoAuditData && (
|
||||
<div className="mt-3 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/30 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
|
||||
>
|
||||
<Download className="h-3 w-3 mr-1.5" /> <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/30 text-amber-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1.5" /> <span className="truncate">Export PDF</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{/* Inline Leads Export - always visible in chat when leads data exists */}
|
||||
{msg.role === "assistant" && msg.agent === "leads" && msg.preview?.data && (
|
||||
<div className="mt-3 flex gap-2 animate-in zoom-in-95 duration-300">
|
||||
<Button
|
||||
onClick={() => { setPreviewData(msg.preview as any); setTimeout(() => exportLeadsReport("html"), 100); }}
|
||||
variant="outline"
|
||||
className="flex-1 bg-emerald-500/10 hover:bg-emerald-500/20 border-emerald-500/30 text-emerald-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
|
||||
>
|
||||
<Download className="h-3 w-3 mr-1.5" /> <span className="truncate">Export HTML</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => { setPreviewData(msg.preview as any); setTimeout(() => exportLeadsReport("csv"), 100); }}
|
||||
variant="outline"
|
||||
className="flex-1 bg-blue-500/10 hover:bg-blue-500/20 border-blue-500/30 text-blue-300 font-black uppercase text-[9px] tracking-wider py-2.5 rounded-xl min-w-0"
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1.5" /> <span className="truncate">Export CSV</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1412,7 +1709,23 @@ export default function AIAssist() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto relative bg-[#050505]">
|
||||
{viewMode === "preview" && currentPreviewData ? (
|
||||
{isModifying ? (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-[#050505]/95 backdrop-blur-xl">
|
||||
<div className="relative mb-6">
|
||||
<div className="w-16 h-16 rounded-full border-[3px] border-blue-500/20 border-t-blue-500 animate-spin" />
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Wrench className="h-6 w-6 text-blue-400 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm font-black text-blue-200 uppercase tracking-[0.2em]">Modification in Progress</p>
|
||||
<p className="text-[10px] text-blue-400/60 mt-2 font-semibold tracking-wider">Rewriting your artifact...</p>
|
||||
<div className="flex items-center gap-1.5 mt-4">
|
||||
{[0, 1, 2].map(i => (
|
||||
<div key={i} className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-bounce" style={{ animationDelay: `${i * 0.15}s` }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : viewMode === "preview" && currentPreviewData ? (
|
||||
<CanvasErrorBoundary>
|
||||
<div className="mx-auto transition-all duration-300 h-full"
|
||||
style={deviceSize !== "full"
|
||||
@@ -1444,9 +1757,9 @@ export default function AIAssist() {
|
||||
{currentPreviewData?.isStreaming ? t.neuralLinkActive : t.syncComplete}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[9px] border-blue-900 text-blue-200/50 font-black">
|
||||
<div className="text-[9px] border-blue-900 text-blue-200/50 font-black">
|
||||
{currentPreviewData?.language?.toUpperCase()} UTF-8
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,8 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
const t = translations[language].sidebar;
|
||||
const common = translations[language].common;
|
||||
|
||||
const menuItems = [
|
||||
const menuItems: Array<{ id: View; label: string; icon: any; count?: number; highlight?: boolean }> = [
|
||||
{ id: "ai-assist" as View, label: t.aiAssist, icon: MessageSquare, highlight: true },
|
||||
{ id: "enhance" as View, label: t.promptEnhancer, icon: Sparkles },
|
||||
{ id: "prd" as View, label: t.prdGenerator, icon: FileText },
|
||||
{ id: "action" as View, label: t.actionPlan, icon: ListTodo },
|
||||
@@ -28,7 +29,6 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
{ id: "slides" as View, label: t.slidesGen, icon: Presentation },
|
||||
{ id: "googleads" as View, label: t.googleAds, icon: Megaphone },
|
||||
{ id: "market-research" as View, label: t.marketResearch, icon: Search },
|
||||
{ id: "ai-assist" as View, label: t.aiAssist, icon: MessageSquare },
|
||||
{ id: "history" as View, label: t.history, icon: History, count: history.length },
|
||||
{ id: "settings" as View, label: t.settings, icon: Settings },
|
||||
];
|
||||
@@ -69,7 +69,8 @@ export default function Sidebar({ currentView, onViewChange }: SidebarProps) {
|
||||
variant={currentView === item.id ? "default" : "ghost"}
|
||||
className={cn(
|
||||
"w-full justify-start gap-2 h-9 lg:h-10 text-sm",
|
||||
currentView === item.id && "bg-primary text-primary-foreground"
|
||||
currentView === item.id && "bg-primary text-primary-foreground",
|
||||
item.highlight && currentView !== item.id && "bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-500/20 text-blue-600 dark:text-blue-400 font-bold"
|
||||
)}
|
||||
onClick={() => handleViewChange(item.id)}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@ export const translations = {
|
||||
googleAds: "Google Ads",
|
||||
uxDesigner: "UX Designer",
|
||||
marketResearch: "Market Research",
|
||||
aiAssist: "AI Assist",
|
||||
aiAssist: "Vibe Architect",
|
||||
settings: "Settings",
|
||||
history: "History",
|
||||
backToRommark: "Back to rommark.dev",
|
||||
@@ -388,8 +388,8 @@ export const translations = {
|
||||
functionalAudit: "Functional Audit",
|
||||
},
|
||||
aiAssist: {
|
||||
title: "AI Assist",
|
||||
description: "Conversational intelligence with agent switching",
|
||||
title: "Vibe Architect",
|
||||
description: "Plan, code, and ship with AI — visual canvas included",
|
||||
placeholder: "Discuss any topic, concern or project...",
|
||||
chatStart: "How can I help you today?",
|
||||
switchingAgent: "Switching to specialized agent...",
|
||||
@@ -434,8 +434,8 @@ export const translations = {
|
||||
inspectCode: "Inspect Code",
|
||||
liveRender: "Live Render",
|
||||
canvasTitle: (type: string) => `${type} Canvas`,
|
||||
studioTitle: "Studio-grade AI Assist",
|
||||
studioDesc: "Switch agents, stream answers, and light up the canvas with live artifacts.",
|
||||
studioTitle: "Vibe Architect",
|
||||
studioDesc: "Describe your vision. Get plans, code, and live previews.",
|
||||
askArtifact: "Ask for a design, code, or research artifact.",
|
||||
suggestions: [
|
||||
{ label: "Build a landing UI", agent: "web" },
|
||||
@@ -448,7 +448,8 @@ export const translations = {
|
||||
design: "Design",
|
||||
seo: "SEO",
|
||||
web: "Web",
|
||||
app: "App"
|
||||
app: "App",
|
||||
leads: "Leads Finder"
|
||||
},
|
||||
userLabel: "Explorer",
|
||||
canvasLabel: "Canvas",
|
||||
@@ -477,7 +478,7 @@ export const translations = {
|
||||
googleAds: "Google Реклама",
|
||||
uxDesigner: "UX Дизайнер",
|
||||
marketResearch: "Анализ рынка",
|
||||
aiAssist: "ИИ Ассистент",
|
||||
aiAssist: "Vibe Architect",
|
||||
settings: "Настройки",
|
||||
history: "История",
|
||||
backToRommark: "Вернуться на rommark.dev",
|
||||
@@ -853,7 +854,7 @@ export const translations = {
|
||||
functionalAudit: "Функциональный аудит",
|
||||
},
|
||||
aiAssist: {
|
||||
title: "ИИ Ассистент",
|
||||
title: "Vibe Architect",
|
||||
description: "Диалоговый интеллект с переключением агентов",
|
||||
placeholder: "Обсудите любую тему, проблему или проект...",
|
||||
chatStart: "Чем я могу помочь вам сегодня?",
|
||||
@@ -899,7 +900,7 @@ export const translations = {
|
||||
inspectCode: "Просмотр кода",
|
||||
liveRender: "Живой рендеринг",
|
||||
canvasTitle: (type: string) => `Холст: ${type}`,
|
||||
studioTitle: "ИИ Ассистент студийного уровня",
|
||||
studioTitle: "Vibe Architect студийного уровня",
|
||||
studioDesc: "Переключайте агентов, получайте ответы и оживляйте холст артефактами.",
|
||||
askArtifact: "Запросите дизайн, код или исследование.",
|
||||
suggestions: [
|
||||
@@ -913,7 +914,8 @@ export const translations = {
|
||||
design: "Дизайн",
|
||||
seo: "SEO",
|
||||
web: "Веб",
|
||||
app: "Приложение"
|
||||
app: "Приложение",
|
||||
leads: "Поиск лидов"
|
||||
},
|
||||
userLabel: "Исследователь",
|
||||
canvasLabel: "Холст",
|
||||
@@ -942,7 +944,7 @@ export const translations = {
|
||||
googleAds: "Google Ads",
|
||||
uxDesigner: "מעצב UX",
|
||||
marketResearch: "מחקר שוק",
|
||||
aiAssist: "סייען AI",
|
||||
aiAssist: "Vibe Architect",
|
||||
settings: "הגדרות",
|
||||
history: "היסטוריה",
|
||||
backToRommark: "חזרה ל-rommark.dev",
|
||||
@@ -1318,7 +1320,7 @@ export const translations = {
|
||||
functionalAudit: "ביקורת פונקציונלית",
|
||||
},
|
||||
aiAssist: {
|
||||
title: "סייען AI",
|
||||
title: "Vibe Architect",
|
||||
description: "אינטליגנציה שיחתית עם החלפת סוכנים",
|
||||
placeholder: "דון בכל נושא, חשש או פרויקט...",
|
||||
chatStart: "במה אוכל לעזור לך היום?",
|
||||
@@ -1364,7 +1366,7 @@ export const translations = {
|
||||
inspectCode: "בדוק קוד",
|
||||
liveRender: "רינדור חי",
|
||||
canvasTitle: (type: string) => `קנבס ${type}`,
|
||||
studioTitle: "סייען AI ברמה מקצועית",
|
||||
studioTitle: "Vibe Architect ברמה מקצועית",
|
||||
studioDesc: "החלף סוכנים, הזרם תשובות והפעל את הקנבס עם ארטיפקטים חיים.",
|
||||
askArtifact: "בקש עיצוב, קוד או מחקר.",
|
||||
suggestions: [
|
||||
@@ -1378,7 +1380,8 @@ export const translations = {
|
||||
design: "עיצוב",
|
||||
seo: "SEO",
|
||||
web: "אינטרנט",
|
||||
app: "אפליקציה"
|
||||
app: "אפליקציה",
|
||||
leads: "חיפוש לידים"
|
||||
},
|
||||
userLabel: "סייר",
|
||||
canvasLabel: "קנבס",
|
||||
|
||||
573
lib/seo-report.ts
Normal file
573
lib/seo-report.ts
Normal file
@@ -0,0 +1,573 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -852,7 +852,13 @@ When the user asks to "Modify", "Change", or "Adjust" something:
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
|
||||
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
|
||||
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
|
||||
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
|
||||
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
|
||||
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
|
||||
**CRITICAL DESIGN REQUIREMENTS:**
|
||||
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
|
||||
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
|
||||
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
|
||||
@@ -865,6 +871,7 @@ AGENTS & CAPABILITIES:
|
||||
- Add animated pulse effects on key metrics
|
||||
- Full-width responsive layout, max-w-4xl mx-auto
|
||||
- Include inline <script> for animating the progress rings on load
|
||||
|
||||
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
|
||||
@@ -509,13 +509,72 @@ When the user asks to "Modify", "Change", or "Adjust" something:
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports. Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>. DARK THEME. Tailwind CDN. Large animated SVG progress rings. Color-coded scoring. Google-style dashboard aesthetics.
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
|
||||
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
|
||||
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
|
||||
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
|
||||
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
|
||||
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
|
||||
**CRITICAL DESIGN REQUIREMENTS:**
|
||||
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
|
||||
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
|
||||
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
|
||||
- Large animated SVG progress rings for scores (Overall, Technical, Content, Mobile) with stroke-dasharray animations
|
||||
- Color-coded scoring: green (#22c55e) for good, amber (#f59e0b) for warning, red (#ef4444) for poor
|
||||
- Use Tailwind CDN for styling. Include: rounded-3xl, shadow-lg, gradient backgrounds
|
||||
- Section cards with subtle borders (border-white/10) and backdrop-blur
|
||||
- Clear visual hierarchy: large score numbers (text-5xl), section titles (text-lg font-bold), bullet points for recommendations
|
||||
- Add a "Key Recommendations" section with icons (use Lucide or inline SVG)
|
||||
- Add animated pulse effects on key metrics
|
||||
- Full-width responsive layout, max-w-4xl mx-auto
|
||||
- Include inline <script> for animating the progress rings on load
|
||||
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
- design: UI/UX Designer. Create high-fidelity mockups and components.
|
||||
- web: Frontend Developer. Build responsive sites. Use [PREVIEW:web:html]. Tailwind CSS CDN by default.
|
||||
- app: Mobile App Developer. Create mobile-first interfaces and dashboards. Use [PREVIEW:app:javascript].
|
||||
- leads: Leads Finder. You are a SILENT data tool. DO NOT chat, greet, ask questions, or explain. Your ONLY job: use [WEB_SEARCH:query] to find leads, then output them as an HTML table. NEVER say "I'd be happy to help" or "Let me search" or "Here are some leads" — just output data.
|
||||
**OUTPUT SEQUENCE (follow exactly):**
|
||||
1. Use [WEB_SEARCH:query] multiple times with different queries to find leads
|
||||
2. One line summary: "Found X leads in Y niche across Z platforms"
|
||||
3. [PREVIEW:leads:html] with the COMPLETE HTML below — replace placeholders with real data
|
||||
4. [/PREVIEW]
|
||||
5. Brief changelog bullet points
|
||||
That is ALL. No greetings, no filler, no conversation.
|
||||
|
||||
**HTML TEMPLATE — copy this exactly, fill in the data:**
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0f0f;color:#e2e8f0;padding:24px}
|
||||
.header{text-align:center;margin-bottom:24px}.header h1{font-size:24px;font-weight:800;background:linear-gradient(135deg,#10b981,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent}.header p{color:#94a3b8;font-size:13px;margin-top:4px}
|
||||
.stats{display:flex;gap:12px;margin-bottom:20px;flex-wrap:wrap}.stat{background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.15);border-radius:10px;padding:10px 16px;text-align:center;flex:1;min-width:100px}.stat .num{font-size:20px;font-weight:800;color:#10b981}.stat .lbl{font-size:10px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;margin-top:2px}
|
||||
table{width:100%;border-collapse:collapse;background:rgba(15,23,42,0.6);border-radius:12px;overflow:hidden;border:1px solid rgba(148,163,184,0.1)}
|
||||
thead th{background:rgba(16,185,129,0.12);color:#10b981;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;padding:12px 14px;text-align:left;border-bottom:1px solid rgba(148,163,184,0.1)}
|
||||
tbody tr{border-bottom:1px solid rgba(148,163,184,0.06)}tbody tr:hover{background:rgba(16,185,129,0.04)}
|
||||
tbody td{padding:10px 14px;font-size:13px;color:#cbd5e1}
|
||||
.name{font-weight:700;color:#f1f5f9}.url{color:#10b981;text-decoration:none;font-size:12px}.url:hover{text-decoration:underline}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600}
|
||||
.badge-instagram{background:rgba(225,48,108,0.15);color:#f472b6}.badge-twitter{background:rgba(59,130,246,0.15);color:#60a5fa}.badge-linkedin{background:rgba(59,130,246,0.15);color:#93c5fd}.badge-youtube{background:rgba(239,68,68,0.15);color:#fca5a5}.badge-tiktok{background:rgba(168,85,247,0.15);color:#c084fc}
|
||||
.followers{color:#10b981;font-weight:700}.region{color:#94a3b8;font-size:12px}.bio{max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
</style></head><body>
|
||||
<div class="header"><h1>Leads Report</h1><p>Generated by PromptArch</p></div>
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="num">TOTAL_COUNT</div><div class="lbl">Total Leads</div></div>
|
||||
<div class="stat"><div class="num">COMBINED_FOLLOWERS</div><div class="lbl">Combined Reach</div></div>
|
||||
<div class="stat"><div class="num">TOP_PLATFORM</div><div class="lbl">Top Platform</div></div>
|
||||
<div class="stat"><div class="num">TOP_REGION</div><div class="lbl">Top Region</div></div>
|
||||
</div>
|
||||
<table><thead><tr><th>#</th><th>Name</th><th>Platform</th><th>Followers</th><th>Region</th><th>Bio</th><th>Link</th></tr></thead><tbody>
|
||||
<tr><td>1</td><td class="name">NAME</td><td><span class="badge badge-PLATFORM">PLATFORM</span></td><td class="followers">FOLLOWERS</td><td class="region">REGION</td><td class="bio" title="FULL BIO">SHORT BIO</td><td><a class="url" href="URL" target="_blank">Visit →</a></td></tr>
|
||||
<!-- Repeat <tr> for each lead. Use badge-instagram/badge-twitter/badge-linkedin/badge-youtube/badge-tiktok -->
|
||||
</tbody></table></body></html>
|
||||
|
||||
**RULES:**
|
||||
- Find 20+ leads. Sort by relevance/follower count.
|
||||
- Use exact follower counts (e.g. "44.1K", "1.2M").
|
||||
- REAL leads from search results only — never fabricate profiles or URLs.
|
||||
- Fill STATS div with real counts from your results.
|
||||
- The entire HTML must be between [PREVIEW:leads:html] and [/PREVIEW].
|
||||
|
||||
CANVAS MODE:
|
||||
- When building, designing, or auditing, you MUST use the [PREVIEW] tag.
|
||||
|
||||
@@ -1134,7 +1134,13 @@ When the user asks to "Modify", "Change", or "Adjust" something:
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports. **CRITICAL DESIGN REQUIREMENTS:**
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
|
||||
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
|
||||
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
|
||||
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
|
||||
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
|
||||
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
|
||||
**CRITICAL DESIGN REQUIREMENTS:**
|
||||
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
|
||||
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
|
||||
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
|
||||
@@ -1147,6 +1153,7 @@ AGENTS & CAPABILITIES:
|
||||
- Add animated pulse effects on key metrics
|
||||
- Full-width responsive layout, max-w-4xl mx-auto
|
||||
- Include inline <script> for animating the progress rings on load
|
||||
|
||||
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
|
||||
@@ -865,7 +865,13 @@ When the user asks to "Modify", "Change", or "Adjust" something:
|
||||
|
||||
AGENTS & CAPABILITIES:
|
||||
- content: Expert copywriter. Use [PREVIEW:content:markdown] for articles, posts, and long-form text.
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.** **CRITICAL DESIGN REQUIREMENTS:**
|
||||
- seo: SEO Specialist. Create stunning SEO audit reports and perform live website analysis. **BEHAVIOR: Stay in SEO mode and handle ALL requests through an SEO lens. Only suggest switching agents for CLEARLY non-SEO tasks (like "write Python backend code") by adding [SUGGEST_AGENT:code] at the END of your response. Never auto-switch mid-response.**
|
||||
**WEB AUDIT CAPABILITIES (use when user provides a URL):**
|
||||
- When user gives a URL (e.g., "audit example.com", "analyze https://site.com"), tell them you will fetch the page data server-side.
|
||||
- You have access to server-side tools. For URL analysis, respond with [WEB_AUDIT:url] where url is the target URL. The system will fetch: title, meta description, headings (h1-h6), internal/external links count, images with/without alt text, canonical URL, Open Graph tags, word count, and HTML structure.
|
||||
- After receiving audit data, produce a comprehensive SEO audit report as a visual dashboard using [PREVIEW:seo:html].
|
||||
- For competitive analysis, keyword research, or SERP analysis: use [WEB_SEARCH:query] to get live search results, then create an analysis report.
|
||||
**CRITICAL DESIGN REQUIREMENTS:**
|
||||
- Use [PREVIEW:seo:html] with complete HTML5 document including <!DOCTYPE html>
|
||||
- DARK THEME: bg-slate-900 or bg-gray-900 as primary background
|
||||
- Google-style dashboard aesthetics with clean typography (use Google Fonts: Inter, Roboto, or Outfit)
|
||||
@@ -878,6 +884,7 @@ AGENTS & CAPABILITIES:
|
||||
- Add animated pulse effects on key metrics
|
||||
- Full-width responsive layout, max-w-4xl mx-auto
|
||||
- Include inline <script> for animating the progress rings on load
|
||||
|
||||
- smm: Social Media Manager. Create multi-platform content plans and calendars.
|
||||
- pm: Project Manager. Create PRDs, timelines, and action plans.
|
||||
- code: Software Architect. Provide logic, algorithms, and backend snippets.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "promptarch",
|
||||
"version": "1.4.0",
|
||||
"version": "1.9.0",
|
||||
"description": "Transform vague ideas into production-ready prompts and PRDs",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Reference in New Issue
Block a user