diff --git a/web/src/padelnomics/app.py b/web/src/padelnomics/app.py index f7ea5bd..82ba62a 100644 --- a/web/src/padelnomics/app.py +++ b/web/src/padelnomics/app.py @@ -280,6 +280,49 @@ def create_app() -> Quart: except Exception as e: return {"status": "unhealthy", "db": str(e)}, 500 + # ------------------------------------------------------------------------- + # Affiliate click redirect — language-agnostic, no blueprint prefix + # ------------------------------------------------------------------------- + + @app.route("/go/") + async def affiliate_redirect(slug: str): + """302 redirect to affiliate URL, logging the click. + + Uses 302 (not 301) so every hit is tracked — browsers don't cache 302s. + Extracts article_slug and lang from Referer header best-effort. + """ + from .affiliate import get_product, log_click + from .core import check_rate_limit + + # Extract lang from Referer path (e.g. /de/blog/... → "de"), default de + referer = request.headers.get("Referer", "") + lang = "de" + article_slug = None + if referer: + try: + from urllib.parse import urlparse + ref_path = urlparse(referer).path + parts = ref_path.strip("/").split("/") + if parts and len(parts[0]) == 2: + lang = parts[0] + if len(parts) > 1: + article_slug = parts[-1] or None + except Exception: + pass + + product = await get_product(slug, lang) + if not product: + abort(404) + + ip = request.remote_addr or "unknown" + allowed, _info = await check_rate_limit(f"aff:{ip}", limit=60, window=60) + if not allowed: + # Still redirect even if rate-limited; just don't log the click + return redirect(product["affiliate_url"], 302) + + await log_click(product["id"], ip, article_slug, referer or None) + return redirect(product["affiliate_url"], 302) + # Legacy 301 redirects — bookmarked/cached URLs before lang prefixes existed @app.route("/terms") async def legacy_terms():