Codebase Audit Report
| Metric | Value | Status |
|---|---|---|
| TypeScript Build | 0 errors | Clean |
| ESLint | 13 errors, 18 warnings | Needs Fix |
| Test Coverage | 0% — no test files | Missing |
| Pages / Routes | 35 pages | Comprehensive |
| API Endpoints | 13 routes | No Rate Limits |
| Tools | 13 interactive tools | Well-built |
| Client Components | 30 of 103 files | Well-split |
| Security Headers | None configured | Missing |
| Error Boundaries | 0 error.tsx / loading.tsx | Missing |
| SEO Metadata | All 35 pages covered | Complete |
The review vote endpoint uses a read-then-update pattern for incrementing helpful_count / not_helpful_count. Two concurrent voters can read the same count value and write back the same incremented value, causing one vote to be silently lost.
UPDATE reviews SET helpful_count = helpful_count + 1 WHERE id = $1, or create a Postgres function and call it with supabase.rpc('increment_vote', ...).
All 13 API endpoints accept unlimited requests. The breach-check endpoint proxies to an external API — abuse could get VPNMarkt's server IP blocked. The newsletter subscribe endpoint has no CAPTCHA and could be abused by spam bots. The review submit and track endpoints allow unlimited writes to the database.
Affected: /api/review/submit, /api/breach-check, /api/track, /api/newsletter/subscribe, /api/review-vote, /api/censorship-check, /api/price-alert, /api/location-search, /api/exchange-rates@vercel/functions rate limiter, or Upstash Redis). Apply stricter limits to write endpoints (5 req/min) and external API proxies (10 req/min).
The site is missing all standard security headers: Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, and Permissions-Policy. Only poweredByHeader: false is set.
headers() async function in next.config.ts returning an array of security headers for all routes. At minimum: HSTS, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin.
The entire app has zero error.tsx files, zero loading.tsx files, and zero Suspense boundaries. If any server component throws (e.g., Supabase is down), users see Next.js's default blank error page with no recovery path. Dynamic pages with data fetching show no loading skeleton during navigation.
src/app/error.tsx (client component) and src/app/loading.tsx as minimum safety nets. Add route-group-specific versions for critical paths like /vpn/[slug] and /compare.
TestResultsDashboard.tsx:100 defines SortIcon as a component inside the render function. React will destroy and recreate this on every render, losing internal state. Additionally, CurrencyProvider.tsx:52 and DealOfTheDay.tsx:15 call setState synchronously inside useEffect, triggering cascading re-renders. There are also 7 unused variable warnings.
The HTML tag is lang="de" but the homepage title, description, OpenGraph, and Twitter metadata are all in English: "Compare, Review & Find VPN Deals". Every other page in the site has proper German metadata. The root layout metadata.title.default is also English, affecting any page that doesn't override it.
Three data-fetching pages lack export const revalidate, making them fully dynamic (SSR on every request): the homepage (page.tsx), the coupons page (coupons/page.tsx), and the quiz (quiz/page.tsx). The homepage is the most-visited page and fetches VPN data from Supabase on every single request.
No test files, no test runner configuration (jest, vitest), no testing dependencies in package.json. 13 API routes with business logic (vote counting, newsletter flow, click tracking, review submission) are completely untested. Quiz scoring, currency conversion, and comparison helpers have zero coverage.
Most page-level Supabase queries destructure only { data }, ignoring the error field. If Supabase is unreachable or returns an error, pages silently render with empty data arrays — showing users a blank page with no VPNs, coupons, or reviews, and no indication anything is wrong.
Design system colors are defined as CSS variables in globals.css but used as string literals in inline style={{}} props throughout the codebase. #171008 appears 100+ times, #f5f0e8 80+ times, #c0311e 60+ times. This makes theme changes extremely painful and error-prone.
Several components handle too many responsibilities: CompareExperience.tsx at 1,319 lines (filtering, URL state, rendering), beste-vpn/[slug]/page.tsx at 931 lines, IpChecker.tsx at 835 lines, vpn/[slug]/page.tsx at 678 lines. These should be broken into focused sub-components.
No next/dynamic imports anywhere in the codebase. Heavy client components like SpeedChart, QuizWizard (664 lines), and CompareExperience (1,319 lines) are loaded synchronously, increasing initial bundle size for pages that embed them.
The newsletter subscribe endpoint generates a confirmation token but then immediately auto-confirms the subscription without sending a verification email. A comment on line 68 says "In production, send confirmation email here". Double opt-in is required by German law (GDPR/UWG §7).
src/app/api/newsletter/subscribe/route.ts:68-73Identical todayCountMap and allCountMap aggregation code is copy-pasted between the VPN detail page and the coupons page. Both loop over click events to build frequency maps using the exact same pattern.
Complex conditional style objects are used extensively instead of Tailwind classes or CSS modules. The Navbar alone has 15+ inline style objects with hardcoded colors. This bypasses Tailwind's utility system, makes components verbose, and prevents effective style reuse.
src/components/layout/Navbar.tsx (15+ inline styles) · Throughout all page.tsx filesThree Google Fonts are loaded (Playfair Display with 6 weights, Lora with 4 weights, IBM Plex Mono with 4 weights). No explicit font-display: swap is set. While Next.js handles some optimization automatically, explicit preloading for the critical Playfair Display font would reduce FOUT risk.
Dynamic routes like /vpn/[slug], /glossar/[slug], and /ratgeber/[slug] don't export generateStaticParams. Pre-generating pages for known slugs would enable static generation with ISR, reducing TTFB for first visitors.
.animate-glow-pulse is defined in globals.css:381 with a comment "no-op in editorial theme" but is never referenced anywhere in the codebase.
Only favicon.ico exists. Missing apple-touch-icon.png, icon-192.png, icon-512.png, and manifest.json for progressive web app support and proper bookmark icons on mobile devices.
The sitemap generator at sitemap.ts:60-63 creates its own createClient() instance directly from @supabase/supabase-js instead of using the shared @/lib/supabase/server helper. This bypasses any middleware or cookie handling that the shared helper provides.
any types in the entire 24K-line codebase. All types are explicit or properly inferred.
'use client', all justified by hooks or browser APIs.
is_approved: true to read; coupons require is_active: true.
CRON_SECRET Bearer token validation on both scheduled endpoints.
Promise.all() across all pages. No N+1 query patterns found.
/nachrichten.
aria-label, aria-expanded, aria-current on navbar. All images have meaningful alt text.
next/image with explicit width/height dimensions. No raw <img> tags anywhere.
.env.local in .gitignore, never committed. Service role key only used server-side.
ToolBreadcrumb, ToolHeader, FaqAccordion, ToolCta, tool-schemas.
| ID | Issue | Location | Severity |
|---|---|---|---|
| vpnmarkt-ts7 | Race condition in review vote counting | api/review-vote/route.ts | Critical |
| vpnmarkt-efe | No rate limiting on any API route | All 13 API routes | Critical |
| vpnmarkt-ia7 | No security headers configured | next.config.ts | Critical |
| vpnmarkt-reo | Zero error boundaries, loading states, Suspense | Entire src/app/ | Critical |
| vpnmarkt-flw | 13 ESLint errors (component-during-render, setState-in-effect) | TestResultsDashboard + 2 more | High |
| vpnmarkt-1s4 | Homepage metadata in English on German site | layout.tsx + page.tsx | High |
| vpnmarkt-7uu | Missing revalidation on 3 dynamic pages | page.tsx, coupons, quiz | High |
| vpnmarkt-396 | Zero test infrastructure | Entire project | High |
| vpnmarkt-8we | Supabase query errors silently swallowed | All page-level queries | High |
| vpnmarkt-7pe | 543 hardcoded color values in style props | Throughout codebase | Medium |
| vpnmarkt-oci | God components exceeding 900 lines | 4 files > 650 lines | Medium |
| vpnmarkt-234 | No code splitting for heavy client components | No next/dynamic usage | Medium |
| vpnmarkt-8fz | Newsletter auto-confirms without email (GDPR risk) | api/newsletter/subscribe | Medium |
| vpnmarkt-shp | Duplicated click tracking aggregation logic | vpn/[slug] + coupons | Medium |
| vpnmarkt-6z9 | 274 inline style objects instead of CSS classes | Throughout codebase | Medium |
| vpnmarkt-o5o | Font loading optimization opportunity | layout.tsx:8-26 | Low |
| vpnmarkt-c4g | Missing generateStaticParams for known slugs | Dynamic [slug] routes | Low |
| vpnmarkt-h6k | Unused CSS class .animate-glow-pulse | globals.css:381 | Low |
| vpnmarkt-bgj | No favicon variants or PWA manifest | public/ | Low |
| vpnmarkt-jt6 | Sitemap creates its own Supabase client | sitemap.ts:60 | Low |
| 20 issues total — 4 critical, 5 high, 6 medium, 5 low | |||
Recommended Fix Priority Order
Week 1 (Critical): Add security headers to next.config.ts. Fix the review vote race condition with an atomic SQL increment. Add root-level error.tsx and loading.tsx. Set up basic rate limiting middleware.
Week 2 (High): Translate homepage metadata to German. Fix ESLint errors (extract SortIcon, refactor setState patterns). Add revalidate to homepage, coupons, and quiz. Start handling Supabase query errors on key pages.
Week 3 (Medium): Set up Vitest + React Testing Library. Write tests for API routes. Extract color constants. Begin breaking up god components. Set up next/dynamic for heavy client components.
Ongoing: Implement double opt-in email flow. Migrate inline styles to CSS classes. Add generateStaticParams. Clean up dead CSS.