BUILD PROMPT — SEO Audit Deck Generator CRITICAL GATE — TOKEN-FIRST (NON-NEGOTIABLE) ──────────────────────────────────────────────────────────────────────────────── Before any actions (scaffold, npm install, npm run dev/build, coding, analysis), the agent MUST: 1. Resolve token in strict order: a) MCP secret source b) runtime env: SE_RANKING_DATA_API_TOKEN c) project .env.local d) sibling/local env files (../*/.env.local, backups) 2. If token is not found: - Stop immediately. - Print this machine-readable blocking line first: Missing SE_RANKING_DATA_API_TOKEN in .env.local and MCP secret source returned no token. - Then print a human-friendly user message (required): SE_RANKING_DATA_API_TOKEN is missing. Please provide one of the following: 1) The SE_RANKING_DATA_API_TOKEN value, or 2) A path to a local env file where it already exists. How to get the token in SE Ranking: 1. Open SE Ranking app 2. In the left sidebar, go to API 3. Open Dashboard 4. Open API Keys 5. Copy Data API Key 6. Share the key here in chat (or share a path to a local env file where it already exists). - Do not run any other commands until user provides token value or env file path. 3. If token is found: - Write/update project .env.local. - Never print token values in chat, logs, terminal output, or UI. - Continue only after token gate passes. 4. Compliance checkpoints (mandatory): - First agent status message: Starting token gate check. - Second status message: Token gate status: PASS or Token gate status: BLOCKED. Any violation means the task is considered incomplete. ========================================= Use this prompt to build the same app from scratch. Paste it into Claude Code (or similar AI coding assistant) in an empty directory. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ GOAL ────── Build a Next.js 15 (App Router) + Python PPTX web app called "SEO Audit Deck Generator". Given a client domain and optional competitor domains it fetches SEO data from the SE Ranking Data API and generates a branded 9-slide PowerPoint file that is downloaded directly by the user. ──────────────────────────────────────────────────────────────────────────────── STACK ──────────────────────────────────────────────────────────────────────────────── - Next.js 15 (App Router, TypeScript, Tailwind CSS) - React 19 - uuid package for job IDs - python-pptx >= 0.6.23 and lxml >= 4.9.0 (Python side, via requirements.txt) - SE Ranking Data API (base URL: https://api.seranking.com/v1) - No database — temp files in OS tmpdir, cleaned up after download ──────────────────────────────────────────────────────────────────────────────── ENVIRONMENT VARIABLES ──────────────────────────────────────────────────────────────────────────────── .env.local (provide .env.example with placeholders): SE_RANKING_DATA_API_TOKEN= # SE Ranking Data API token (Authorization: Token ) N8N_API_KEY= # Optional - protects the /api/n8n/generate endpoint NEXT_PUBLIC_BASE_URL= # Optional - used by n8n route for download URL construction TOKEN-FIRST EXECUTION ORDER (MANDATORY) ──────────────────────────────────────────────────────────────────────────────── Do not duplicate token rules across this prompt. Single source of truth is the top section: `CRITICAL GATE — TOKEN-FIRST (NON-NEGOTIABLE)`. Practical lookup limits (still mandatory): 1. Do not run broad recursive scans like `find ..` without depth/file limits. 2. Search sibling env files with bounded scope only: - known sibling paths first (for example `../pdf-report-generator/.env.local`, `../audit-deck-builder/.env.local`, `../ai-visibility-checker/.env.local`) - then max depth 2, filename `.env.local` only. 3. Keep token discovery check under 10 seconds total; if not found quickly, stop search and ask user for token value or env path. BASE PATH REQUIREMENT (MANDATORY): - Final handoff URL must include this basePath: /audit-deck-builder - Preferred full URL (when 3000 is free): http://localhost:3000/audit-deck-builder - Configure `next.config.ts`: - `basePath: '/audit-deck-builder'` - `env.NEXT_PUBLIC_BASE_PATH = '/audit-deck-builder'` - All client API calls and download links must use `${process.env.NEXT_PUBLIC_BASE_PATH}/api/...` (never hardcode bare `/api/...` when basePath is enabled). ──────────────────────────────────────────────────────────────────────────────── FILE STRUCTURE ──────────────────────────────────────────────────────────────────────────────── / ├── app/ │ ├── layout.tsx # Root layout (dark navy background, global CSS) │ ├── page.tsx # UI: form → SSE progress → download button │ ├── globals.css # Tailwind + @keyframes spin + progress-pulse │ └── api/ │ ├── generate/route.ts # GET — SSE streaming deck generation │ ├── download/route.ts # GET — streams .pptx file, cleans up temp files │ ├── n8n/generate/route.ts # POST — synchronous version for n8n automation │ └── debug/route.ts # GET — returns raw SE Ranking API data as JSON (dev only) ├── lib/ │ ├── types.ts # All TypeScript interfaces │ ├── seranking.ts # SE Ranking API client │ └── recommendations.ts # Generates top 3 priority recommendations ├── scripts/ │ └── generate_deck.py # Python PPTX slide builder ├── requirements.txt # python-pptx>=0.6.23, lxml>=4.9.0 └── .env.example ──────────────────────────────────────────────────────────────────────────────── SE RANKING API CLIENT — lib/seranking.ts ──────────────────────────────────────────────────────────────────────────────── CRITICAL RULES (do not skip these — we learned them the hard way): 1. All API calls must be SEQUENTIAL. No Promise.all / parallel fetches. 2. Enforce an 800ms sleep before EVERY API call. 3. On HTTP 429: exponential backoff — wait 1s, 2s, 4s, 8s (up to 4 retries). 4. Auth header: Authorization: Token 5. Throw a readable error on non-2xx responses that includes the status and body. Implement these exported functions: getCompetitors(domain, source) GET /v1/domain/competitors?source=&domain=&type=organic&stats=1 Returns Competitor[]. Response may be a plain array or { data: [], competitors: [] }. getDomainOverview(domain, source) GET /v1/domain/overview/db?source=&domain=&with_subdomains=1 Returns DomainOverview. The organic field comes back as an ARRAY — normalise to a single object by taking arr[0]. Key fields: keywords_count, traffic_sum. getDomainKeywords(domain, source, limit=20) GET /v1/domain/keywords?source=&domain=&type=organic&limit=&order_field=traffic_percent&order_type=desc IMPORTANT: use order_field + order_type (NOT order_by — that returns 400). Response is a plain JSON array (not wrapped). Real field names on each row: keyword, position, prev_pos (null = new), volume (NOT search_volume), traffic_percent, url, cpc, difficulty. Return { total, data: KeywordRow[] }. getBacklinksSummary(domain) GET /v1/backlinks/summary?target=&mode=host Response wraps in { summary: [...] } — unwrap and return summary[0]. Real field name: refdomains (NOT referring_domains). getAIFullAnalysis(domain, competitors, country) Fetches AI visibility data across 5 engines: ai-overview, ai-mode, chatgpt, gemini, perplexity. For the CLIENT domain, call fetchGlobalAIOverview() per engine: GET /v1/ai-search/overview?target=&source=&engine=&scope=base_domain IMPORTANT: the engine parameter is REQUIRED — a call without it returns 400. The response structure is nested: { summary: { brand_presence: { current: N }, link_presence: { current: N }, average_position: { current: N }, ai_opportunity_traffic: { current: N } }, time_series: {...} } Read summary.brand_presence.current etc. — NOT top-level fields. This endpoint returns global database counts, which match what is shown on the SE Ranking platform. Do NOT use the leaderboard for client metrics because leaderboard scopes counts to the comparison set (lower numbers). For COMPETITORS, call fetchLeaderboardPerEngine() per engine: POST /v1/ai-search/overview/leaderboard Body: { primary: { target, brand }, competitors: [{ target, brand }], scope: "base_domain", source: country, engines: [engine] } The client entry has is_primary_target: true. For competitor entries match on !is_primary_target + domain string contains. Extract fields: brand_presence → mentions, link_presence → citations. If fetchGlobalAIOverview fails for an engine, fall back to reading the is_primary_target entry from the leaderboard response. Returns AIFullAnalysis: { client_engines: AIEngineRow[], competitor_engines: AICompetitorData[] } runSiteAudit(domain, maxWaitMs=220000, pollMs=5000) POST /v1/site-audit/audits/standard with { domain, settings: { max_pages: 100 } } Returns audit ID. Then poll GET /v1/site-audit/audits (the list endpoint) and find the item by ID — the per-audit status endpoint does not work reliably. Poll until audit leaves in-progress states (queued/pending/processing/running) or timeout. Parse metrics from nested `stats` first: score <- stats.score pages_crawled <- stats.crawled issues <- stats.errors / stats.warnings / stats.notices Return { score, pages_crawled, issues }. buildCompetitiveGap(clientKeywords, competitors, source, limit=5) For each competitor (up to 5) call getDomainKeywords(competitor, source, 30). Collect keywords where the client has no presence (not in client keyword set) AND volume > 100. Deduplicate keeping highest volume per keyword. Sort by search_volume desc, return top 20. ──────────────────────────────────────────────────────────────────────────────── TYPE DEFINITIONS — lib/types.ts ──────────────────────────────────────────────────────────────────────────────── export interface AuditFormInput { domain: string country: string // SE Ranking source code, e.g. "us", "gb" competitors: string[] } export interface DomainOverview { organic: { keywords_count: number traffic_sum: number price_sum?: number keywords_new_count?: number keywords_up_count?: number keywords_down_count?: number keywords_lost_count?: number top1_5?: number; top6_10?: number; top11_20?: number; top21_50?: number; top51_100?: number } adv?: { keywords_count: number; traffic_sum: number; price_sum?: number } } export interface Competitor { domain: string common_keywords?: number; organic_keywords?: number organic_traffic?: number; organic_cost?: number; relevance?: number } export interface KeywordRow { keyword: string position: number prev_pos: number | null // null = new / not tracked volume: number // monthly search volume — field is "volume" NOT "search_volume" traffic?: number; traffic_percent?: number; url?: string competition?: number; cpc?: number; difficulty?: number } export interface DomainKeywordsResponse { total: number; data: KeywordRow[] } export interface BacklinksSummary { backlinks: number refdomains: number // "refdomains" NOT "referring_domains" subnets?: number; ips?: number nofollow_backlinks?: number; dofollow_backlinks?: number; dofollow_refdomains?: number inlink_rank?: number; domain_inlink_rank?: number } export interface AIEngineRow { engine: string // 'ai-overview' | 'ai-mode' | 'chatgpt' | 'gemini' | 'perplexity' mentions?: number // brand_presence.current from /ai-search/overview citations?: number // link_presence.current avg_position?: number // average_position.current opp_traffic?: number // ai_opportunity_traffic.current } export interface AICompetitorData { domain: string; engines: AIEngineRow[] } export interface AIFullAnalysis { client_engines: AIEngineRow[] competitor_engines: AICompetitorData[] } export interface SiteAuditReport { score: number; pages_crawled?: number issues?: { errors: number; warnings: number; notices: number } top_issues?: { id?: string; title?: string; description?: string; count: number; severity?: string }[] } export interface CompetitiveGapKeyword { keyword: string; search_volume: number // sourced from KeywordRow.volume competitor_domain: string; competitor_position: number; client_position: number | null } export interface RecommendationItem { dimension: string; issue: string; action: string; impact_score: number } export interface AuditDeckData { domain: string; country: string; generated_at: string competitors: string[] overview: DomainOverview | null keywords: { total: number; top_10: KeywordRow[]; drops: KeywordRow[]; visibility_percent: number } | null backlinks: BacklinksSummary | null ai_visibility: AIFullAnalysis | null site_audit: SiteAuditReport | null competitive_gap: CompetitiveGapKeyword[] recommendations: RecommendationItem[] errors: Record // endpoint → error message, for graceful degradation } ──────────────────────────────────────────────────────────────────────────────── MAIN ROUTE — app/api/generate/route.ts (SSE streaming) ──────────────────────────────────────────────────────────────────────────────── GET endpoint with export const dynamic = 'force-dynamic' and maxDuration = 300. Reads params from URL query string: domain, country, skip_audit, competitors[]. Streams Server-Sent Events in format: data: \n\n Event shapes: { type: 'progress', step, percent } | { type: 'done', jobId } | { type: 'api_errors', errors } | { type: 'error', message } Pipeline (send a progress event after each step): 1. Competitors — auto-detect if none provided (getCompetitors), or use provided list 2. Domain overview (getDomainOverview) 3. Keywords (getDomainKeywords limit=20) — extract top_10, drops (prev_pos < position), visibility 4. Backlinks (getBacklinksSummary) 5. AI visibility (getAIFullAnalysis) 6. Site audit (runSiteAudit) — only if skip_audit is false; default is to skip 7. Competitive gap (buildCompetitiveGap using top_10 keywords) 8. Recommendations (generateRecommendations) 9. Write data JSON to tmpdir, spawn python3 generate_deck.py Write meta JSON to tmpdir: { domain, pptxFile, createdAt } Stream 'done' event with jobId Each step is in a try/catch — on error, store to errors dict and continue. After all data is gathered, send an api_errors event if errors dict is non-empty. ──────────────────────────────────────────────────────────────────────────────── DOWNLOAD ROUTE — app/api/download/route.ts ──────────────────────────────────────────────────────────────────────────────── GET /api/download?jobId= Read meta JSON from tmpdir, read pptx file, return as attachment with Content-Type: application/vnd.openxmlformats-officedocument.presentationml.presentation Filename format: -audit-.pptx Clean up both temp files after 5 seconds. ──────────────────────────────────────────────────────────────────────────────── N8N ROUTE — app/api/n8n/generate/route.ts ──────────────────────────────────────────────────────────────────────────────── POST endpoint — same pipeline as the SSE route but synchronous (no streaming). Protected by Authorization: Bearer header check. Body: { domain, country?, competitors?, skip_audit?, clientEmail?, managerEmail? } Response: { jobId, domain, downloadUrl, filename, clientEmail, managerEmail, apiErrors? } downloadUrl = /api/download?jobId= ──────────────────────────────────────────────────────────────────────────────── RECOMMENDATIONS — lib/recommendations.ts ──────────────────────────────────────────────────────────────────────────────── generateRecommendations(data: AuditDeckData): RecommendationItem[] Score and rank candidates across 5 dimensions, return top 3. Pad to exactly 3 with generic fallbacks if fewer real signals exist. Dimensions and scoring: Site Health — if score < 90: impact = (100 - score) * 2.5 AI Visibility — if totalClientMentions < avgCompetitorMentions: impact = min(gap/avg * 70, 70) Find engine with biggest absolute gap Organic Drops — if drops exist: impact = min(totalDropVolume * 0.008, 80) Competitive Gap — if gap keywords exist: impact = min(totalVolume * 0.005, 75) Backlinks — if refdomains < 50: impact = max(60 - refdomains * 0.8, 20) if links/domain ratio > 50: impact = min(ratio * 0.8, 50) Fallback candidates (impact_score = 0): Content Strategy, Technical SEO, Local/Brand Authority ──────────────────────────────────────────────────────────────────────────────── PYTHON SLIDE BUILDER — scripts/generate_deck.py ──────────────────────────────────────────────────────────────────────────────── Called as: python3 generate_deck.py Slide deck (9 slides, 13.33" × 7.5" widescreen): Slide 1 — Cover Large domain name, "SEO Audit Report", date, "Powered by SE Ranking" Dark navy background, white text Slide 2 — Executive Summary 3 KPI cards side by side (4 cards if site audit was run): KPI 1 — SITE HEALTH (only if audit ran): score/100, colour red/yellow/green KPI 2 — AI MENTIONS: total mentions across all 5 engines, colour-coded KPI 3 — SEARCH VISIBILITY: organic.traffic_sum formatted as K/M shorthand (e.g. 42.3K, 1.2M) — this matches "Estimated Monthly Traffic" on SE Ranking Subtitle: "est. monthly organic visits" + ranked keywords count Label "SEARCH VISIBILITY" NOT "Keyword Visibility" KPI 4 (if no audit) / KPI 4 — shows competitor count One-sentence interpretation below the cards Slide 3 — AI Visibility Two side-by-side tables: LEFT: Client per-engine — Engine | Brand Mentions | Link Citations | Avg Position Colour-code: green if mentions > 50, yellow if > 10, red otherwise RIGHT: "Where to Double Down" — Engine | You | Top Competitor | Action Sort by gap (competitor mentions - client mentions) descending Action text generated in Python based on gap size Subtitle: "current tracked period" (API returns current window, not all-time cumulative) Slide 4 — Organic Rankings Table of top 10 keywords — 4 columns ONLY: Keyword | Position | Monthly Searches | Traffic % DO NOT include a "Change" column — prev_pos data is often null and adds no value Highlight rows in light red where keyword is in the drops set Caption showing how many keywords dropped positions Slide 5 — Site Health (only rendered if audit was run) Health score gauge (large number), error/warning/notice counts Top issues list Slide 6 — Backlinks Overview Key metrics: total backlinks, referring domains, dofollow count, domain authority Note on link profile quality Slide 7 — Competitive Landscape Table: competitor domains with their organic keywords + traffic from SE Ranking Slide 8 — Competitive Gap Table of top 20 gap keywords — Keyword | Search Volume | Best Competitor | Their Position Slide 9 — Recommendations Exactly 3 recommendation cards, each with: Dimension badge (colour-coded), Issue description, Action to take Design system for Python PPTX: NAVY = RGBColor(0x1B, 0x3A, 0x5C) BLUE = RGBColor(0x29, 0x80, 0xB9) GREEN = RGBColor(0x27, 0xAE, 0x60) YELLOW = RGBColor(0xF3, 0x9C, 0x12) ORANGE = RGBColor(0xE6, 0x7E, 0x22) RED = RGBColor(0xE7, 0x4C, 0x3C) WHITE = RGBColor(0xFF, 0xFF, 0xFF) DARK = RGBColor(0x2C, 0x3E, 0x50) MID_GRAY= RGBColor(0x7F, 0x8C, 0x8D) LIGHT_GRAY = RGBColor(0xF5, 0xF6, 0xFA) ROW_ALT = RGBColor(0xF8, 0xF9, 0xFA) Every data slide has: - Thin navy top bar with slide title (white bold) + subtitle (lighter text) - Thin navy footer bar with domain name and page number - One sentence interpretation box at the bottom above the footer ──────────────────────────────────────────────────────────────────────────────── FRONT-END UI — app/page.tsx ──────────────────────────────────────────────────────────────────────────────── Single-page client component. Dark navy-to-slate gradient background. Centered white card with three states: IDLE / ERROR state — form: - Client Domain input (required) - Country/Market dropdown (16 countries, value = SE Ranking source code: us, gb, ca, au...) - 5 competitor domain inputs (optional — leave blank to auto-detect) - "Include Site Health Audit" checkbox (default OFF, warns it adds ~3 min) - "Generate Audit Deck" submit button (navy) GENERATING state: - Spinning SVG with percentage number in centre - Progress bar - Current step label (from SSE progress events) - SSE connection to /api/generate with form values as query params DONE state: - Green checkmark - "Download .pptx" button (triggers /api/download?jobId=) - Collapsible warning if any API data sources failed - "Generate another deck" reset link ──────────────────────────────────────────────────────────────────────────────── KNOWN API GOTCHAS (critical — bake these in from the start) ──────────────────────────────────────────────────────────────────────────────── 1. getDomainKeywords: use order_field=traffic_percent&order_type=desc NOT order_by=traffic_percent — the latter returns HTTP 400. 2. SE Ranking domain overview returns organic as an ARRAY [{}], not an object. Always normalise: if Array.isArray(arr) return arr[0]. 3. BacklinksSummary field is "refdomains" not "referring_domains". 4. KeywordRow volume field is "volume" not "search_volume". 5. /ai-search/overview requires engine= parameter. Omitting it returns: {"error":["engine parameter is required"]}. Call once per engine. 6. /ai-search/overview response is nested: { summary: { brand_presence: { current: N }, ... } } NOT flat. Read raw.summary.brand_presence.current etc. 7. API returns current-period AI counts; SE Ranking platform shows cumulative all-time. Expect API numbers to be lower — this is expected, not a bug. Document it in the slide subtitle as "current tracked period". 8. Site audit status polling: use GET /v1/site-audit/audits (the list) and find item by ID. The individual audit status endpoint is unreliable. 9. All SE Ranking API calls must be sequential with 800ms delay. The API blocks parallel requests from the same token. ──────────────────────────────────────────────────────────────────────────────── SETUP INSTRUCTIONS FOR THE PERSON RUNNING THIS ──────────────────────────────────────────────────────────────────────────────── MANDATORY EXECUTION POLICY (for AI coding agent): - Do NOT stop after code generation. Finish end-to-end: install deps, verify env, run build, start dev server, confirm final URL works. - Prefer doing actions automatically. Ask the user only for: 1) permission to run privileged/system commands (installers, kill old process, etc.), 2) missing secrets (token value) that cannot be discovered safely. - Keep user interruptions minimal. Avoid optional pre-check questions. - Do not ask for confirmation before routine read-only checks (`lsof`, `cat`, `rg`) or normal project commands (`npm install`, `npm run build`, `npm run dev`) when permissions allow. DEPENDENCY CHECK + AUTO-INSTALL POLICY (MANDATORY): 1. Check required tools: - node, npm, python3, pip3 2. If missing, auto-install with user permission: - macOS: prefer Homebrew (install Homebrew first if needed) - if Node/npm missing: install Node LTS (nvm or brew) 3. Then run: - npm install - pip3 install -r requirements.txt 4. If pip wheel/build fails on macOS: - ask user to install Command Line Tools (`xcode-select --install`) and retry. 5. When command asks sudo password: - explicitly tell user password input is hidden (no characters appear), type blindly and press Enter. - remind user to open their local Terminal app (macOS Terminal/iTerm, Windows PowerShell/Terminal). 6. Presentation tooling: - no separate GUI presentation tool is required. - required deck generator dependencies are only `python-pptx` and `lxml` from `requirements.txt`. TOKEN & LANGUAGE NOTES (NO DUPLICATION): - Do not repeat token acquisition instructions here. - Follow only the opening token gate section at the top of this prompt. - Keep all user-facing messages in the user's language; technical labels remain in English. - Final handoff line must also be in the user's language. FINAL URL MESSAGE FORMAT (MANDATORY): - In successful handoff, output exactly 2 lines in this order: 1) Short completion sentence in the user's language. 2) One clean URL as plain text (no Markdown link wrapper, no brackets, no duplicated URL). - English example sentence for line 1: `Done. Open this link in your browser:` RUNTIME + PORT POLICY (MANDATORY): - Preferred handoff URL: http://localhost:3000/audit-deck-builder - If port 3000 is occupied: - do NOT interrupt user with extra confirmation, - auto-start on the next free port (3001, 3002, ...), - return exactly one final working URL with the actual port. - Only stop another process on 3000 if the user explicitly requests strict 3000 binding. - Do not run redundant pre/final curl checks when server logs already confirm startup. Use curl checks only if user asks or if there are signs of failure. AUTO-RECOVERY / KNOWN ISSUES (MANDATORY): 1) TypeScript issue in `app/api/generate/route.ts`: - Avoid fragile conditional infer indexing like `T['data']`. - Use explicit type: `let topKeywords: KeywordRow[] = []` and import `KeywordRow` from `lib/types`. 2) Next.js runtime cache error (example: `Cannot find module '/873.js'`): - stop dev server, - remove `.next`, - run `npm run build`, - run `npm run dev` again. - treat this as known local dev cache corruption (not user input error, not API data issue). 3) Hydration mismatch with `data-gr-ext-installed` / `data-new-gr-c-s-check-loaded`: - usually browser extension mutation (e.g., Grammarly), not app logic bug. - verify in clean/incognito profile or disable extension before code changes. 4) 404 due to malformed URL formatting in chat handoff: - do not output broken markdown like `http://localhost:3000/audit-deck-builder](http:/localhost:3000/audit-deck-builder`. - URL line must be plain text only, for example: `http://localhost:3000/audit-deck-builder` - do not duplicate base path and do not wrap URL with mismatched brackets. 5) API partial errors from missing token: - show clear human message in UI: "Missing SE_RANKING_DATA_API_TOKEN in .env.local. Add it and restart dev server." 6) Site Audit parsing correctness (prevent false `0/100`): - when polling `/v1/site-audit/audits`, parse score and counters from nested `stats` object first: - `stats.score`, `stats.errors`, `stats.warnings`, `stats.notices`, `stats.crawled` - use top-level fields only as fallback when present. - do not interpret `0 errors/warnings/notices` as `100/100` score; use API score value only. 7) Competitor table completeness (prevent `—` placeholders): - do not hardcode dashes in Competitive Landscape. - for each competitor include real `keywords` and `traffic` from: - `/v1/domain/competitors` stats when available, or - fallback `/v1/domain/overview/db` per competitor. - if metric is missing after both attempts, then show `—` and add non-blocking warning in `api_errors`. 8) Recommendation card text clipping: - enable text wrapping in Python PPTX text frames (`text_frame.word_wrap = True`) - keep 3 cards but ensure issue/action text is visible without truncation in standard 16:9 deck. 9) Site audit timeout degradation: - if site audit does not complete within timeout, do not fail whole deck generation. - continue deck generation with partial warning: `site_audit: Site audit timed out before completion` - in this case, render Site Health slide in "audit unavailable" mode with a clear explanatory note. 10) Token lookup hangs on unbounded sibling scan: - treat as known issue, not as token absence by default. - avoid unbounded recursive search; use bounded lookup only (known sibling paths first, then `maxdepth=2` for `.env.local`). - if search exceeds time budget, abort and ask user for token value or env path immediately. FINAL HANDOFF CHECKLIST (MANDATORY): 1. `npm run build` passes. 2. Dev server is running. 3. Required URL opens at the active port with basePath `/audit-deck-builder` (prefer 3000). 4. Final response uses mandatory 2-line format: localized completion sentence + one plain-text URL line. 5. UI flow works: form -> generate -> download. 6. If any non-blocking API source failed, show readable warning in UI (not raw stack trace). 7. Validate generated PPT content quality: - Competitive Landscape shows numeric metrics for competitors (when API returns them), - Site Health score/counters are consistent with Site Audit API payload, - Recommendation text is fully readable (not cut off). Manual quick-start (for a human running without an AI agent): 1. npm install 2. pip3 install -r requirements.txt 3. Copy .env.example to .env.local and set `SE_RANKING_DATA_API_TOKEN` 4. npm run dev 5. Open http://localhost:/audit-deck-builder 6. Enter domain, optional competitors, click Generate For production deploy on Vercel: - Set SE_RANKING_DATA_API_TOKEN in environment variables - Set NEXT_PUBLIC_BASE_URL to your production URL - Python must be available in the runtime (use a custom runtime or self-host) - maxDuration = 300 requires Vercel Pro or higher (5-minute function timeout) PATCH NOTES — 2026-04-24 (POST-IMPLEMENTATION GUARDRAILS) These guardrails are mandatory and override ambiguous behavior in older runs. A) Site Audit timeout must NEVER render fake score values - If Site Audit times out, do NOT render `0/100` or any synthetic score. - Render Site Health as `N/A` with a clear note like: "Audit unavailable in this run (timed out)." - Keep generation non-blocking and expose warning in UI/api_errors. - Recommendations must ignore Site Health scoring when audit is unavailable. B) Competitor metrics must be hydrated even for manually entered competitors - If user provides competitors manually, still fetch their metrics. - Data priority: 1. `/v1/domain/competitors` stats when present 2. fallback `/v1/domain/overview/db` per competitor - Only show `—` when both sources fail; add non-blocking warning in `api_errors`. C) Deck language must be English-only - All slide text, interpretations, recommendation Issue/Action text, and fallback notes must be English. - Keep user-facing chat/status language localized, but PPT content must remain English. D) `/v1/site-audit/audits` response shape normalization - The audits list may be returned as array OR wrapped object (`data`/`audits`). - Normalize before matching by ID to avoid false timeout handling. E) Skip-audit behavior must be explicit (no fake health score) - If user leaves "Include Site Health Audit" OFF (`skip_audit=true`), do NOT show `0/100`. - Render explicit message in deck context: "Site audit was not requested for this run." - If Site Health slide is rendered in this mode, score must be `N/A` (not numeric) with a clear explanatory note. - Recommendations must not treat skipped audit as a low health score signal. G) Site Audit timeout policy for stable runs - Site Audit can time out due to API queue/load even with a valid token. - Use `maxWaitMs >= 420000` (7 minutes) for `runSiteAudit` to reduce false timeouts. - Keep graceful degradation mandatory: if timeout occurs, continue deck generation with a readable warning in UI/API. - For fast demo/iteration runs, prefer `skip_audit=true` (Site Health audit disabled). H) SSE stream stability + HTML error hygiene (mandatory) - In `app/api/generate/route.ts`, avoid double-closing stream controllers. - Guard `controller.enqueue` and `controller.close` with a local closed flag and safe try/catch. - Never allow unhandledRejection from SSE route finalization. - In `app/page.tsx`, wrap `fetch` + stream reading in try/catch. - If HTTP response is non-OK and body is HTML (Next error page), convert it to a short readable message (do not render raw HTML blob in UI). - If stream closes before `done`, switch UI to error state and stop loader. I) Runtime cache corruption recovery (strict sequence) - Symptom examples: `Cannot find module './873.js'` or similar missing chunk errors in `.next/server/*`. - Recovery sequence (exact order): 1. Stop dev server. 2. Remove `.next`. 3. Run `npm run build`. 4. Start dev server again. - Treat this as local runtime cache corruption, not API/business-logic failure. J) Change-scope rule for future runs - If user asks to "update instruction file", apply only instruction-file edits unless user explicitly requests app code changes. - Before touching app code, first confirm the user wants code changes. K) Webpack runtime crash signature (dev) - If error appears as `__webpack_modules__[moduleId] is not a function`, treat it as local Next.js dev runtime/cache corruption. - Apply the same strict recovery sequence from section I (stop dev -> remove `.next` -> `npm run build` -> `npm run dev`). - Do not interpret this error as API data failure or token/config issue. L) Site Audit deduplication + cooldown (mandatory) - Before creating a new Site Audit (`POST /v1/site-audit/audits/standard`), first check `/v1/site-audit/audits` list. - If there is an existing in-progress audit for the same domain (`queued/pending/processing/running/in_progress`), reuse/poll that audit ID instead of creating a new one. - If there is a recently completed audit for the same domain (recommended cooldown: up to 60 minutes) with valid stats, reuse that result instead of creating a new audit. - Goal: prevent duplicate audit jobs and repeated email notifications from SE Ranking when user retries generation. - Keep graceful degradation unchanged: if reused/new audit does not finish within timeout, continue deck with readable `site_audit` warning. M) Universal Site Audit dedupe/cooldown implementation notes - Treat Site Audit creation as an expensive side-effect operation. - Before creating a new audit job, always query the audits list and normalize all known list wrappers (`items`, `data`, `audits`, or plain array). - Domain matching must be tolerant across response shapes: check multiple possible fields such as `domain`, `target`, `url`, `title`, and nested project/site domain fields. - If a matching audit is currently in progress, poll/reuse it instead of creating a new job. - If a matching audit was completed recently (recommended window: 30-60 minutes) and includes valid stats, reuse it instead of creating a new job. - Keep a short in-process cooldown guard to avoid duplicate job creation when timestamp precision is coarse. - Purpose: reduce duplicate background audits, repeated provider notifications, and unnecessary long waits on retries. - This is operational behavior only and must remain domain-agnostic. N) Stable local fallback when dev runtime is unstable - If `next dev` repeatedly crashes with local runtime/cache errors even after recovery sequence, use production-like local run for verification: 1. `npm run build` 2. `npm run start` - Use this fallback only for stability checks; keep normal iterative development on `npm run dev` when stable. O) Date-only audit timestamps handling (mandatory) - Some Site Audit list responses may expose recency via date-only fields (for example `last_update` as `YYYY-MM-DD` without time). - Recency logic must support this format explicitly. - If a matching audit has date-only timestamp equal to "today", treat it as reusable for dedupe/cooldown purposes. - Do not create a new audit job when a same-day completed audit is already available and contains valid stats. - Goal: prevent repeated audit creation and repeated provider email notifications on same-day retries. P) Site Health slide issue-table completeness (mandatory) - Do not leave Site Health issue table empty when aggregate issue counts are present. - If audits list payload provides only summary counters but no issue rows, enrich from audit report endpoint (`/site-audit/audits/report` for the selected audit id). - Build `top_issues` from report sections/issue props using available fields (issue name/code, severity/status, count/value). - Include only issue rows with positive counts and valid severity mapping. - Sort by issue count descending (with severity priority tie-breaker) and render top rows in the slide table. - Keep this enrichment non-blocking: if report enrichment fails, preserve score/counters and emit a readable warning in `api_errors`. Q) Strict parsing rules for Site Audit dedupe and issue enrichment - Audits list normalization must support all common wrappers: `items`, `data`, `audits`, and plain array. - Domain matching for audit reuse must check multiple fields: `domain`, `target`, `url`, `title`, and nested project/site domain fields. - Recency logic must support both datetime and date-only stamps: - datetime examples: `updated_at`, `finished_at`, `created_at` - date-only example: `last_update` in `YYYY-MM-DD` - Date-only `last_update` equal to current day must be treated as same-day reusable audit for dedupe/cooldown. - `top_issues` enrichment source must be `/site-audit/audits/report?id=`. - Parse issues from `sections[*].props[*]` and map fields as: - title from `name` (fallback `code`/key), - severity from `status`, - count from `value`. - Include only rows with `count > 0` and valid severity (`error|warning|notice`). - Keep enrichment non-blocking: base score/counters must still render if report parsing fails. R) UI credit-consumption note near Generate button - Add a short user-facing note near the start button informing about typical run cost. - Recommended wording: `Note: a run usually consumes about 48,200 SE Ranking Data API credits.` - Keep the note visible in idle state before generation starts. - Treat this as informational guidance (approximate), not a strict billing guarantee.