Production QA Validator
Run phases in order. Fix failures before moving to next.
When to Use
- Use before shipping or promoting a fullstack Next.js app to production.
- Use after large UI, SEO, auth, API, database, or dependency changes need a concrete launch-readiness pass.
- Use when you need a compact command-driven checklist for build, route, metadata, performance, security, and cleanup checks.
export PROD_URL="https://yourdomain.com"
export QA_AUTH_HEADER="" # optional: "Bearer eyJ..."
export PAGESPEED_API_KEY="" # optional: for auto PageSpeed API
Consolidated Runner
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; }
Phase 1: Code Integrity
qa:code() { npx tsc --noEmit && npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 && npm test -- --runInBand --passWithNoTests; }
Phase 2: Build Verification
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 |
Phase 3: API Session & Authentication
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
}
Phase 4: Route Regression
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')"; }
Phase 5: SEO — Tags, Images, Favicon, Slugs
qa: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"
}
Phase 6: API Route Behavior
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"
}
Phase 7: Git Hygiene
qa: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"
}
Phase 8: Post-Deployment Smoke Test
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"
}
Phase 9: Page Speed, Lazy Load & Bundles
qa: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"
Phase 10: Cleanup & Vulnerability Scan
qa: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)
[ -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"
}
Phase 11: UI/UX — Cards, Animation, Error Boundaries
qa: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"
}
Phase 12: Database & Data Layer
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"
}
Phase 13: Secure Data Rendering
qa: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"
}
Pre-Commit Hook
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
CI/CD (GitHub Actions)
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
Best Practices
| ✅ 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 |
Common Pitfalls
| 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 |
Security Notes
- All
qa:* functions are read-only (tsc, lint, test, build, curl, grep)
PROD_URL and QA_AUTH_HEADER only for enprojectnments you own
- Basic secret scanning in
git diff — for prod, use trufflehog/git-secrets
- Auth tests with real credentials against prod is destructive — use staging
Limitations
- Passing all phases reduces risk but doesn't eliminate production bugs
- Some checks depend on project-specific tooling (Prisma, NextAuth, etc.)
- Manual UX testing still required for critical user journeys
- SEO checks verify raw HTML only — not social preview rendering
- Route checks verify status codes, not content correctness
Master Checklist
Phase 1: Code
Phase 2: Build
Phase 3: Auth
Phase 4: Routes
Phase 5: SEO
Phase 6: API
Phase 7: Git
Phase 8: Smoke
Phase 9: Speed
Phase 10: Clean
Phase 11: UI/UX
Phase 12: Database
Phase 13: Secure Rendering
1---2name: vibecode-production-qa-validator3description: 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.4---56# Production QA Validator78Run phases in order. Fix failures before moving to next.910## When to Use1112- Use before shipping or promoting a fullstack Next.js app to production.13- Use after large UI, SEO, auth, API, database, or dependency changes need a concrete launch-readiness pass.14- Use when you need a compact command-driven checklist for build, route, metadata, performance, security, and cleanup checks.1516```bash17export PROD_URL="https://yourdomain.com"18export QA_AUTH_HEADER="" # optional: "Bearer eyJ..."19export PAGESPEED_API_KEY="" # optional: for auto PageSpeed API20```2122---2324## Consolidated Runner2526```bash27qa: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; }28qa: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; }29```3031---3233### Phase 1: Code Integrity3435- [ ] `npx tsc --noEmit`36- [ ] `npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0`37- [ ] `npm test -- --runInBand --passWithNoTests`3839```bash40qa:code() { npx tsc --noEmit && npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 && npm test -- --runInBand --passWithNoTests; }41```4243---4445### Phase 2: Build Verification4647- [ ] `npm run build` succeeds48- [ ] SEO pages show `○`/`●` not `λ`49- [ ] Build log has no errors5051```bash52qa: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"; }53```5455| Symbol | Meaning |56|--------|---------|57| `○` | Static |58| `●` | SSG |59| `λ` | Dynamic/serverless |60| `⊕` | Partial prerender |6162---6364### Phase 3: API Session & Authentication6566- [ ] Auth endpoints respond (login, session, logout)67- [ ] Protected routes return 401/40368- [ ] Session cookie: HttpOnly + Secure + SameSite69- [ ] Cookie not expired, Path/Domain correct70- [ ] No rate limiting bypass7172```bash73qa:auth() {74 local F=075 for ep in /api/auth/login /api/auth/session /api/auth/logout; do76 curl -so /dev/null -w "%{http_code}" "$PROD_URL$ep" | grep -q "200\|401" || { echo " ✗ $ep unreachable"; ((F++)); }77 done78 curl -so /dev/null -w "%{http_code}" "$PROD_URL/api/protected" | grep -q "401\|403" || echo " ⚠ Protected route not denying unauthenticated"79 return $F80}81qa:auth:cookies() {82 for ep in /api/auth/session /api/auth/login; do83 curl -sI "$PROD_URL$ep" | grep -i "^set-cookie:" | while IFS= read -r c; do84 echo " $ep: $(echo "$c" | cut -d= -f1)"85 echo "$c" | grep -qi "HttpOnly" || echo " ✗ Missing HttpOnly"86 echo "$c" | grep -qi "Secure" || echo " ✗ Missing Secure"87 echo "$c" | grep -qi "SameSite" || echo " ⚠ Missing SameSite"88 done89 done90}91```9293---9495### Phase 4: Route Regression9697- [ ] Core pages, sitemap, robots.txt all 20098- [ ] URLs use kebab-case, no duplicate slugs99- [ ] robots.txt allows indexing100- [ ] Sitemap XML valid, all URLs resolve 200101102```bash103qa: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; }104qa:robots() { curl -s "$PROD_URL/robots.txt" | grep -qi "Disallow: /$" && echo " ✗ Blocks all crawlers" || echo " ✓ OK"; }105qa:sitemap() { curl -s "$PROD_URL/sitemap.xml" | python3 -c "import sys,xml.etree.ElementTree as ET; ET.parse(sys.stdin); print('✓ Valid XML')"; }106```107108---109110### Phase 5: SEO — Tags, Images, Favicon, Slugs111112- [ ] `<title>` 30–60 chars, unique per page113- [ ] `<meta name="description">` in raw HTML114- [ ] og:title matches `<title>`, og:url matches canonical115- [ ] og:image ≥ 1200×630px, absolute URL, loads 200116- [ ] twitter:card = summary_large_image117- [ ] Canonical self-referencing, no duplicates118- [ ] `/favicon.ico` 200, apple-touch-icon present119- [ ] `hreflang` tags if multilingual120- [ ] JSON-LD structured data present121- [ ] Slugs: kebab-case, < 80 chars, no stop words122123```bash124qa:seo() {125 local H=$(curl -s "$PROD_URL"); local F=0126 for t in "og:title" "og:description" "og:image" "twitter:card" "canonical" "description"; do echo "$H" | grep -qi "$t" || { echo " ✗ $t"; ((F++)); }; done127 echo "$H" | grep -qi "<title>" || { echo " ✗ <title>"; ((F++)); }128 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)"129 curl -so /dev/null -w "%{http_code}" "$PROD_URL/favicon.ico" | grep -q 200 || echo " ⚠ No favicon.ico"130 return $F131}132qa:seo:ogimage() {133 local I=$(curl -s "$PROD_URL" | grep -oP 'og:image" content="\K[^"]+'); [[ "$I" =~ ^http ]] || I="$PROD_URL$I"134 curl -so /dev/null -w "%{http_code}" "$I" | grep -q 200 || { echo " ✗ og:image returns non-200"; return 1; }135 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"136}137```138139---140141### Phase 6: API Route Behavior142143- [ ] Correct status codes + Content-Type144- [ ] Errors return consistent JSON `{ error, message }`145- [ ] Response times < 200ms146- [ ] CORS headers correct (if cross-origin)147148```bash149qa:api() {150 for p; do151 local R=$(curl -so /dev/null -w "%{http_code} %{content_type}" "$PROD_URL$p")152 echo " $p → $R"153 done154 local E=$(curl -s "$PROD_URL/api/nonexistent")155 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"156}157```158159---160161### Phase 7: Git Hygiene162163- [ ] No secrets/credentials in diff164- [ ] No `.next`/`node_modules` staged165- [ ] Commit: `type(scope): message`166167```bash168qa:git() {169 local S=$(git diff HEAD 2>/dev/null | grep -i "password\|secret\|api_key\|localhost:3000" | grep "^+")170 [ -n "$S" ] && { echo " ✗ Secrets in diff!"; echo "$S"; return 1; } || echo " ✓ No secrets"171 local A=$(git status --short 2>/dev/null | grep -E "\.next|node_modules" | head -3)172 [ -n "$A" ] && echo " ⚠ Build artifacts:" && echo "$A" || echo " ✓ No artifacts"173}174```175176---177178### Phase 8: Post-Deployment Smoke Test179180- [ ] Homepage 200, key pages 200181- [ ] OG image loads 200182- [ ] No console errors (manual)183- [ ] Auth flow works (manual)184185```bash186qa:smoke() {187 curl -sI "$PROD_URL" | head -1 | grep -q "200" && echo " ✓ Homepage" || echo " ✗ Homepage"188 curl -sI "$PROD_URL/sitemap.xml" | head -1 | grep -q "200" && echo " ✓ Sitemap" || echo " ✗ Sitemap"189}190```191192---193194### Phase 9: Page Speed, Lazy Load & Bundles195196- [ ] Lighthouse ≥ 90 (Perf, A11y, SEO)197- [ ] FCP < 2.5s, LCP < 4.0s, CLS < 0.1198- [ ] Images lazy-loaded (`loading="lazy"`), WebP/AVIF199- [ ] Dynamic imports for heavy components200- [ ] Largest JS chunk < 200KB gzipped201- [ ] `font-display: swap`, no FOIT202- [ ] Total page weight < 1MB203204```bash205qa:lazyload() {206 local N=$(grep -r "loading=" app/ --include="*.tsx" 2>/dev/null | grep -c "lazy" || true)207 echo " Lazy images: $N"208 grep -rn "next/dynamic\|dynamic((" app/ --include="*.tsx" 2>/dev/null | head -5 | grep . || echo " ⚠ No dynamic imports"209}210qa:heavyload() {211 ls -lhS .next/static/chunks/*.js 2>/dev/null | head -5212 local W=$(curl -so /dev/null -w "%{size_download}" "$PROD_URL" 2>/dev/null || echo 0)213 echo " HTML weight: ~$((W/1024))KB"214 echo " ⚠ Run 'npx lighthouse $PROD_URL --view' for full weight analysis"215}216# PageSpeed: open "https://pagespeed.web.dev/?url=$PROD_URL"217```218219---220221### Phase 10: Cleanup & Vulnerability Scan222223- [ ] `npm prune`, `depcheck` — no unused deps224- [ ] No console.log/debugger in staged code225- [ ] `npm audit` — zero critical/high vulnerabilities226- [ ] No eval/new Function/document.write227- [ ] TODOs resolved228229```bash230qa:vulns() {231 npm audit 2>/dev/null | grep -E "critical|high" | grep . && echo " ✗ Vulnerabilities!" || echo " ✓ No critical/high vulns"232 npm outdated 2>/dev/null | head -5 | grep . || echo " ✓ All up to date"233 local D=$(grep -rn "eval(\|new Function(\|document.write(" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -5)234 [ -n "$D" ] && echo " ⚠ Dangerous patterns:" && echo "$D" || echo " ✓ No dangerous patterns"235}236qa:cleanup() {237 local D=$(git diff --cached 2>/dev/null | grep "^+" | grep -i "console\.log\|debugger" | head -5)238 [ -n "$D" ] && echo " ✗ Debug artifacts:" && echo "$D" || echo " ✓ No debug artifacts"239 local T=$(git diff --cached 2>/dev/null | grep "^+" | grep -i "TODO\|FIXME\|HACK" | head -5)240 [ -n "$T" ] && echo " ⚠ TODOs remain:" && echo "$T"241}242```243244---245246### Phase 11: UI/UX — Cards, Animation, Error Boundaries247248- [ ] Cards: equal height grid, no overlap, text ellipsis, responsive (1→2→3 col)249- [ ] No horizontal scroll at any viewport (320–1440px)250- [ ] Images: consistent `aspect-ratio` + `object-fit: cover`251- [ ] Touch targets ≥ 44×44px252- [ ] Animations use `transform`+`opacity` only (not layout props)253- [ ] `prefers-reduced-motion` respected254- [ ] Error boundaries at root + route level (`app/error.tsx`, `app/global-error.tsx`)255- [ ] `app/not-found.tsx` and `app/loading.tsx` exist256- [ ] All client fetches show loading + error + empty states257- [ ] Buttons: hover, focus-visible, active, disabled, loading states258- [ ] Forms disable submit on click (no double-submit)259260```bash261qa:ux:cards() {262 local E=$(grep -rn "text-overflow\|line-clamp\|truncate" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)263 [ -n "$E" ] && echo " ✓ Text overflow handling" || echo " ⚠ No text overflow handling"264 local A=$(grep -rn "aspect-\|object-fit" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)265 [ -n "$A" ] && echo " ✓ aspect-ratio/object-fit used" || echo " ⚠ No aspect-ratio set"266}267qa:ux:boundaries() {268 for f in app/error.tsx app/global-error.tsx app/not-found.tsx app/loading.tsx; do269 [ -f "$f" ] && echo " ✓ $f" || echo " ⚠ Missing $f"270 done271}272qa:ux:animation() {273 local A=$(grep -rn "animation.*width\|transition.*height\|@keyframes.*top\|@keyframes.*margin" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -5)274 [ -n "$A" ] && echo " ⚠ Layout-triggering animations:" && echo "$A" || echo " ✓ No layout-triggering animations"275 local P=$(grep -r "@media.*prefers-reduced-motion" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)276 [ -n "$P" ] && echo " ✓ prefers-reduced-motion found in CSS" || echo " ⚠ No prefers-reduced-motion in CSS"277}278```279280---281282### Phase 12: Database & Data Layer283284- [ ] Connection pool configured (no starvation)285- [ ] Schema in sync with migrations286- [ ] Indexes on all queried columns, no N+1287- [ ] No hardcoded DB credentials in source288- [ ] No raw SQL injection risk289- [ ] No sensitive data leaked in API responses290- [ ] Migrations are idempotent291292```bash293qa:database() {294 local H=$(grep -rn "postgres://\|mysql://\|mongodb://" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v ".env" | head -5)295 [ -n "$H" ] && { echo " ✗ Hardcoded DB URL:"; echo "$H"; } || echo " ✓ No hardcoded DB URLs"296 local R=$(grep -rn "\$queryRaw\|\.raw(" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -5)297 [ -n "$R" ] && echo " ⚠ Raw SQL:" && echo "$R" || echo " ✓ No raw SQL"298 local N=$(grep -rn "\.findMany\|\.findUnique" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "include:" | head -5)299 [ -n "$N" ] && echo " ⚠ Possible N+1:" && echo "$N" || echo " ✓ No N+1 patterns"300}301qa:db:migrations() {302 [ -d "prisma/migrations" ] && echo " ✓ Prisma: $(ls prisma/migrations 2>/dev/null | wc -l) migrations" || echo " - No prisma migrations dir"303 local M=$(ls db/migrations/*.sql 2>/dev/null | head -5); [ -n "$M" ] && echo " ✓ SQL migrations:" && echo "$M" || echo " - No SQL migration files"304}305```306307---308309### Phase 13: Secure Data Rendering310311- [ ] No secrets/tokens in client source or localStorage312- [ ] No `dangerouslySetInnerHTML` without DOMPurify313- [ ] API errors don't leak stack traces314- [ ] Internal IDs use UUIDs not auto-increment315- [ ] User emails masked in UI316- [ ] NEXT_PUBLIC_ vars contain no secrets317318```bash319qa:secure() {320 local S=$(git grep -n "api_key\|API_KEY\|secret_key\|PRIVATE_KEY" -- ':!*.env*' ':!*test*' 2>/dev/null | head -5)321 [ -n "$S" ] && echo " ✗ Secrets in source:" && echo "$S" || echo " ✓ No hardcoded secrets"322 local D=$(grep -rn "dangerouslySetInnerHTML" app/ src/ --include="*.tsx" 2>/dev/null | head -5)323 [ -n "$D" ] && echo " ⚠ XSS risk — use DOMPurify:" && echo "$D" || echo " ✓ No dangerouslySetInnerHTML"324 local T=$(grep -rn "localStorage\|sessionStorage" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -i "token\|jwt\|secret" | head -5)325 [ -n "$T" ] && echo " ⚠ Tokens in storage — use httpOnly cookies:" && echo "$T" || echo " ✓ No tokens in storage"326 curl -s "$PROD_URL/api/nonexistent" 2>/dev/null | grep -qi "stack\|Error:" && echo " ✗ Stack trace leak" || echo " ✓ No stack leak"327}328```329330---331332## Pre-Commit Hook333334```bash335cat > .git/hooks/pre-commit << 'EOF'336#!/bin/sh337npx tsc --noEmit || exit 1338npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 || exit 1339EOF340chmod +x .git/hooks/pre-commit341```342343---344345## CI/CD (GitHub Actions)346347```yaml348name: QA349on: [push, pull_request]350jobs:351 qa:352 runs-on: ubuntu-latest353 steps:354 - uses: actions/checkout@v4355 - uses: actions/setup-node@v4356 - run: npm ci357 - run: npx tsc --noEmit358 - run: npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0359 - run: npm test -- --runInBand --passWithNoTests360 - run: npm run build361```362363---364365## Best Practices366367| ✅ Do | ❌ Don't |368|-------|----------|369| Run full 13-phase flow before deploy | Skip typecheck or lint |370| Set `PROD_URL` in profile/.envrc | Hardcode URLs in scripts |371| OG images ≥ 1200×630 | Use small OG images |372| Animate with `transform`+`opacity` | Animate width/height/top |373| Show loading/error/empty states | Leave users on blank screens |374| `prefers-reduced-motion` for animations | Force motion on all users |375| HttpOnly + Secure cookies for tokens | localStorage for auth tokens |376| Error boundaries at all levels | White screen on crash |377| Database indexes + include/populate | N+1 queries in loops |378| `npm audit` before deploy | Deploy with known vulns |379380---381382## Common Pitfalls383384| Problem | Solution |385|---------|----------|386| OG tags missing in raw HTML | Use `export const metadata` in Next.js |387| `Disallow: /` in robots.txt | Blocks all crawlers — use specific paths |388| Cards different heights in grid | Use `display: grid` with equal-height rows, not flex |389| Text overflows card | Add `text-overflow: ellipsis` + `overflow: hidden` |390| Animation jank | Animate `transform` not `width`/`height` |391| Form submits twice | Disable button on first click |392| Console errors in prod | Add `no-console` ESLint rule |393| DB connection timeout | Add connection pooling (PgBouncer/Prisma Accelerate) |394| Sensitive data in API | Strip `passwordHash`/`secret` in response transformer |395| App crashes on error | Add `app/error.tsx` error boundary |396| Large JS bundles | Dynamic import heavy components, analyze with `next/bundle-analyzer` |397| Images load slowly | Add `loading="lazy"`, use WebP/AVIF, resize to display size |398399---400401## Security Notes402403- All `qa:*` functions are read-only (tsc, lint, test, build, curl, grep)404- `PROD_URL` and `QA_AUTH_HEADER` only for enprojectnments you own405- Basic secret scanning in `git diff` — for prod, use `trufflehog`/`git-secrets`406- Auth tests with real credentials against prod is destructive — use staging407408---409410## Limitations411412- Passing all phases reduces risk but doesn't eliminate production bugs413- Some checks depend on project-specific tooling (Prisma, NextAuth, etc.)414- Manual UX testing still required for critical user journeys415- SEO checks verify raw HTML only — not social preview rendering416- Route checks verify status codes, not content correctness417418---419420## Master Checklist421422### Phase 1: Code423- [ ] `tsc --noEmit`, `eslint`, `npm test` pass424425### Phase 2: Build426- [ ] `npm run build` succeeds, no errors, pages static427428### Phase 3: Auth429- [ ] Endpoints respond, protected routes denied, secure cookies430431### Phase 4: Routes432- [ ] All core pages 200, sitemap valid, robots.txt correct433434### Phase 5: SEO435- [ ] title, description, og:*, twitter:card, canonical, favicon, slugs436437### Phase 6: API438- [ ] Status, Content-Type, consistent errors, timing439440### Phase 7: Git441- [ ] No secrets, no artifacts, conventional commit442443### Phase 8: Smoke444- [ ] Homepage + key pages 200, og:image loads445446### Phase 9: Speed447- [ ] Lighthouse ≥ 90, lazy images, dynamic imports, font-display: swap448449### Phase 10: Clean450- [ ] No vulns, no debug artifacts, unused deps pruned451452### Phase 11: UI/UX453- [ ] Cards responsive, error boundaries, button states, reduced-motion454455### Phase 12: Database456- [ ] Indexes, no N+1, no hardcoded URLs, no sensitive leaks457458### Phase 13: Secure Rendering459- [ ] No secrets in client, no XSS, no stack leaks, UUIDs