bundled-skills/vibecode-production-qa-validator/SKILL.md
13-phase production QA for fullstack Next.js apps: build verification, SEO tags, OG images, favicon, route regression, API auth, page speed, lazy load, vulnerability scan, UI/UX cards, error boundaries, database, secure rendering, and cleanup.
npx skillsauth add FrancoStino/opencode-skills-antigravity vibecode-production-qa-validatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Run phases in order. Fix failures before moving to next.
export PROD_URL="https://yourdomain.com"
export QA_AUTH_HEADER="" # optional: "Bearer eyJ..."
export PAGESPEED_API_KEY="" # optional: for auto PageSpeed API
qa:all() { qa:code && qa:build && qa:routes / /about /contact /privacy /terms /faq /sitemap.xml /robots.txt /api/health && qa:seo && qa:api /api/health /api/tools && qa:git && qa:smoke; }
qa:full() { qa:all && qa:auth && qa:auth:cookies && qa:lazyload && qa:heavyload && qa:vulns && qa:cleanup && qa:ux:cards && qa:ux:boundaries && qa:ux:animation && qa:database && qa:secure; }
npx tsc --noEmitnpx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0npm test -- --runInBand --passWithNoTestsqa:code() { npx tsc --noEmit && npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 && npm test -- --runInBand --passWithNoTests; }
npm run build succeeds○/● not λqa:build() { local log; log="$(mktemp "${TMPDIR:-/tmp}/qa-build.XXXXXX.log")" || return 1; set -o pipefail; npm run build 2>&1 | tee "$log"; local rc=$?; set +o pipefail; [ "$rc" -eq 0 ] && ! grep -qi "error\|failed" "$log"; local ok=$?; rm -f "$log"; return "$ok"; }
| Symbol | Meaning |
|--------|---------|
| ○ | Static |
| ● | SSG |
| λ | Dynamic/serverless |
| ⊕ | Partial prerender |
qa:auth() {
local F=0
for ep in /api/auth/login /api/auth/session /api/auth/logout; do
curl -so /dev/null -w "%{http_code}" "$PROD_URL$ep" | grep -q "200\|401" || { echo " ✗ $ep unreachable"; ((F++)); }
done
curl -so /dev/null -w "%{http_code}" "$PROD_URL/api/protected" | grep -q "401\|403" || echo " ⚠ Protected route not denying unauthenticated"
return $F
}
qa:auth:cookies() {
for ep in /api/auth/session /api/auth/login; do
curl -sI "$PROD_URL$ep" | grep -i "^set-cookie:" | while IFS= read -r c; do
echo " $ep: $(echo "$c" | cut -d= -f1)"
echo "$c" | grep -qi "HttpOnly" || echo " ✗ Missing HttpOnly"
echo "$c" | grep -qi "Secure" || echo " ✗ Missing Secure"
echo "$c" | grep -qi "SameSite" || echo " ⚠ Missing SameSite"
done
done
}
qa:routes() { local F=0; for p; do local C=$(curl -so /dev/null -w "%{http_code}" "$PROD_URL$p"); echo "$C $p"; [ "$C" = "200" ] || ((F++)); done; return $F; }
qa:robots() { curl -s "$PROD_URL/robots.txt" | grep -qi "Disallow: /$" && echo " ✗ Blocks all crawlers" || echo " ✓ OK"; }
qa:sitemap() { curl -s "$PROD_URL/sitemap.xml" | python3 -c "import sys,xml.etree.ElementTree as ET; ET.parse(sys.stdin); print('✓ Valid XML')"; }
<title> 30–60 chars, unique per page<meta name="description"> in raw HTML<title>, og:url matches canonical/favicon.ico 200, apple-touch-icon presenthreflang tags if multilingualqa:seo() {
local H=$(curl -s "$PROD_URL"); local F=0
for t in "og:title" "og:description" "og:image" "twitter:card" "canonical" "description"; do echo "$H" | grep -qi "$t" || { echo " ✗ $t"; ((F++)); }; done
echo "$H" | grep -qi "<title>" || { echo " ✗ <title>"; ((F++)); }
local T=$(echo "$H" | grep -oP '<title>\K[^<]+'); local L=${#T}; [ $L -ge 30 -a $L -le 60 ] || echo " ⚠ Title ${L}chars (target 30-60)"
curl -so /dev/null -w "%{http_code}" "$PROD_URL/favicon.ico" | grep -q 200 || echo " ⚠ No favicon.ico"
return $F
}
qa:seo:ogimage() {
local I=$(curl -s "$PROD_URL" | grep -oP 'og:image" content="\K[^"]+'); [[ "$I" =~ ^http ]] || I="$PROD_URL$I"
curl -so /dev/null -w "%{http_code}" "$I" | grep -q 200 || { echo " ✗ og:image returns non-200"; return 1; }
command -v identify &>/dev/null && curl -s "$I" | identify -format "%wx%h" - 2>/dev/null | grep -qP "12\d{2}x6\d{2}" && echo " ✓ ≥ 1200x630" || echo " ⚠ Install imagemagick to check dimensions"
}
{ error, message }qa:api() {
for p; do
local R=$(curl -so /dev/null -w "%{http_code} %{content_type}" "$PROD_URL$p")
echo " $p → $R"
done
local E=$(curl -s "$PROD_URL/api/nonexistent")
echo "$E" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'error' in d; print('✓ Consistent errors')" 2>/dev/null || echo " ⚠ Inconsistent error shape"
}
.next/node_modules stagedtype(scope): messageqa:git() {
local S=$(git diff HEAD 2>/dev/null | grep -i "password\|secret\|api_key\|localhost:3000" | grep "^+")
[ -n "$S" ] && { echo " ✗ Secrets in diff!"; echo "$S"; return 1; } || echo " ✓ No secrets"
local A=$(git status --short 2>/dev/null | grep -E "\.next|node_modules" | head -3)
[ -n "$A" ] && echo " ⚠ Build artifacts:" && echo "$A" || echo " ✓ No artifacts"
}
qa:smoke() {
curl -sI "$PROD_URL" | head -1 | grep -q "200" && echo " ✓ Homepage" || echo " ✗ Homepage"
curl -sI "$PROD_URL/sitemap.xml" | head -1 | grep -q "200" && echo " ✓ Sitemap" || echo " ✗ Sitemap"
}
loading="lazy"), WebP/AVIFfont-display: swap, no FOITqa:lazyload() {
local N=$(grep -r "loading=" app/ --include="*.tsx" 2>/dev/null | grep -c "lazy" || true)
echo " Lazy images: $N"
grep -rn "next/dynamic\|dynamic((" app/ --include="*.tsx" 2>/dev/null | head -5 | grep . || echo " ⚠ No dynamic imports"
}
qa:heavyload() {
ls -lhS .next/static/chunks/*.js 2>/dev/null | head -5
local W=$(curl -so /dev/null -w "%{size_download}" "$PROD_URL" 2>/dev/null || echo 0)
echo " HTML weight: ~$((W/1024))KB"
echo " ⚠ Run 'npx lighthouse $PROD_URL --view' for full weight analysis"
}
# PageSpeed: open "https://pagespeed.web.dev/?url=$PROD_URL"
npm prune, depcheck — no unused depsnpm audit — zero critical/high vulnerabilitiesqa:vulns() {
npm audit 2>/dev/null | grep -E "critical|high" | grep . && echo " ✗ Vulnerabilities!" || echo " ✓ No critical/high vulns"
npm outdated 2>/dev/null | head -5 | grep . || echo " ✓ All up to date"
local D=$(grep -rn "eval(\|new Function(\|document.write(" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -5) # security-allowlist: defensive source scan
[ -n "$D" ] && echo " ⚠ Dangerous patterns:" && echo "$D" || echo " ✓ No dangerous patterns"
}
qa:cleanup() {
local D=$(git diff --cached 2>/dev/null | grep "^+" | grep -i "console\.log\|debugger" | head -5)
[ -n "$D" ] && echo " ✗ Debug artifacts:" && echo "$D" || echo " ✓ No debug artifacts"
local T=$(git diff --cached 2>/dev/null | grep "^+" | grep -i "TODO\|FIXME\|HACK" | head -5)
[ -n "$T" ] && echo " ⚠ TODOs remain:" && echo "$T"
}
aspect-ratio + object-fit: covertransform+opacity only (not layout props)prefers-reduced-motion respectedapp/error.tsx, app/global-error.tsx)app/not-found.tsx and app/loading.tsx existqa:ux:cards() {
local E=$(grep -rn "text-overflow\|line-clamp\|truncate" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)
[ -n "$E" ] && echo " ✓ Text overflow handling" || echo " ⚠ No text overflow handling"
local A=$(grep -rn "aspect-\|object-fit" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)
[ -n "$A" ] && echo " ✓ aspect-ratio/object-fit used" || echo " ⚠ No aspect-ratio set"
}
qa:ux:boundaries() {
for f in app/error.tsx app/global-error.tsx app/not-found.tsx app/loading.tsx; do
[ -f "$f" ] && echo " ✓ $f" || echo " ⚠ Missing $f"
done
}
qa:ux:animation() {
local A=$(grep -rn "animation.*width\|transition.*height\|@keyframes.*top\|@keyframes.*margin" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -5)
[ -n "$A" ] && echo " ⚠ Layout-triggering animations:" && echo "$A" || echo " ✓ No layout-triggering animations"
local P=$(grep -r "@media.*prefers-reduced-motion" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)
[ -n "$P" ] && echo " ✓ prefers-reduced-motion found in CSS" || echo " ⚠ No prefers-reduced-motion in CSS"
}
qa:database() {
local H=$(grep -rn "postgres://\|mysql://\|mongodb://" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v ".env" | head -5)
[ -n "$H" ] && { echo " ✗ Hardcoded DB URL:"; echo "$H"; } || echo " ✓ No hardcoded DB URLs"
local R=$(grep -rn "\$queryRaw\|\.raw(" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -5)
[ -n "$R" ] && echo " ⚠ Raw SQL:" && echo "$R" || echo " ✓ No raw SQL"
local N=$(grep -rn "\.findMany\|\.findUnique" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "include:" | head -5)
[ -n "$N" ] && echo " ⚠ Possible N+1:" && echo "$N" || echo " ✓ No N+1 patterns"
}
qa:db:migrations() {
[ -d "prisma/migrations" ] && echo " ✓ Prisma: $(ls prisma/migrations 2>/dev/null | wc -l) migrations" || echo " - No prisma migrations dir"
local M=$(ls db/migrations/*.sql 2>/dev/null | head -5); [ -n "$M" ] && echo " ✓ SQL migrations:" && echo "$M" || echo " - No SQL migration files"
}
dangerouslySetInnerHTML without DOMPurifyqa:secure() {
local S=$(git grep -n "api_key\|API_KEY\|secret_key\|PRIVATE_KEY" -- ':!*.env*' ':!*test*' 2>/dev/null | head -5)
[ -n "$S" ] && echo " ✗ Secrets in source:" && echo "$S" || echo " ✓ No hardcoded secrets"
local D=$(grep -rn "dangerouslySetInnerHTML" app/ src/ --include="*.tsx" 2>/dev/null | head -5)
[ -n "$D" ] && echo " ⚠ XSS risk — use DOMPurify:" && echo "$D" || echo " ✓ No dangerouslySetInnerHTML"
local T=$(grep -rn "localStorage\|sessionStorage" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -i "token\|jwt\|secret" | head -5)
[ -n "$T" ] && echo " ⚠ Tokens in storage — use httpOnly cookies:" && echo "$T" || echo " ✓ No tokens in storage"
curl -s "$PROD_URL/api/nonexistent" 2>/dev/null | grep -qi "stack\|Error:" && echo " ✗ Stack trace leak" || echo " ✓ No stack leak"
}
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
npx tsc --noEmit || exit 1
npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 || exit 1
EOF
chmod +x .git/hooks/pre-commit
name: QA
on: [push, pull_request]
jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx tsc --noEmit
- run: npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0
- run: npm test -- --runInBand --passWithNoTests
- run: npm run build
| ✅ Do | ❌ Don't |
|-------|----------|
| Run full 13-phase flow before deploy | Skip typecheck or lint |
| Set PROD_URL in profile/.envrc | Hardcode URLs in scripts |
| OG images ≥ 1200×630 | Use small OG images |
| Animate with transform+opacity | Animate width/height/top |
| Show loading/error/empty states | Leave users on blank screens |
| prefers-reduced-motion for animations | Force motion on all users |
| HttpOnly + Secure cookies for tokens | localStorage for auth tokens |
| Error boundaries at all levels | White screen on crash |
| Database indexes + include/populate | N+1 queries in loops |
| npm audit before deploy | Deploy with known vulns |
| Problem | Solution |
|---------|----------|
| OG tags missing in raw HTML | Use export const metadata in Next.js |
| Disallow: / in robots.txt | Blocks all crawlers — use specific paths |
| Cards different heights in grid | Use display: grid with equal-height rows, not flex |
| Text overflows card | Add text-overflow: ellipsis + overflow: hidden |
| Animation jank | Animate transform not width/height |
| Form submits twice | Disable button on first click |
| Console errors in prod | Add no-console ESLint rule |
| DB connection timeout | Add connection pooling (PgBouncer/Prisma Accelerate) |
| Sensitive data in API | Strip passwordHash/secret in response transformer |
| App crashes on error | Add app/error.tsx error boundary |
| Large JS bundles | Dynamic import heavy components, analyze with next/bundle-analyzer |
| Images load slowly | Add loading="lazy", use WebP/AVIF, resize to display size |
qa:* functions are read-only (tsc, lint, test, build, curl, grep)PROD_URL and QA_AUTH_HEADER only for environments you owngit diff — for prod, use trufflehog/git-secretstsc --noEmit, eslint, npm test passnpm run build succeeds, no errors, pages staticdata-ai
Snapshot a site's SEO state and detect ranking, indexation, metadata, canonical, robots, schema, and on-page regressions over time.
development
Coordinate focused subagents on substantial work, keep their ownership non-overlapping, and integrate verified results. Use for large-scope Codex tasks; keep trivial work with the coordinator.
data-ai
Use when an owner asks to find a cofounder or project partner. Assess only that agent's own owner and rank only approved profiles other agents posted for their own owners.
devops
Install, configure, verify, repair, update, and uninstall Hyprland on Fedora Linux with GPU-aware detection (NVIDIA/AMD/Intel).