import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { ATTRIBUTION_COOKIE_NAME, buildAttributionSnapshot, serializeAttributionCookie, } from '@/lib/revops'; import { verifySignedUserIdEdge } from '@/lib/session-edge'; const isProduction = process.env.NODE_ENV === 'production'; function attachAttributionCookie(req: NextRequest, response: NextResponse) { if (req.cookies.get(ATTRIBUTION_COOKIE_NAME)?.value) { return response; } const path = req.nextUrl.pathname; if (path.startsWith('/api/') || path.startsWith('/_next') || path.startsWith('/r/') || path.includes('.')) { return response; } const snapshot = buildAttributionSnapshot({ utmSource: req.nextUrl.searchParams.get('utm_source'), utmMedium: req.nextUrl.searchParams.get('utm_medium'), utmCampaign: req.nextUrl.searchParams.get('utm_campaign'), utmContent: req.nextUrl.searchParams.get('utm_content'), utmTerm: req.nextUrl.searchParams.get('utm_term'), referrer: req.headers.get('referer'), landingPath: path, firstSeenAt: new Date(), }); response.cookies.set(ATTRIBUTION_COOKIE_NAME, serializeAttributionCookie(snapshot), { httpOnly: false, secure: isProduction, sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 90, }); return response; } export async function middleware(req: NextRequest) { const path = req.nextUrl.pathname; const hostname = req.headers.get('host')?.split(':')[0] || req.nextUrl.hostname; if (hostname === 'qrmaster.net') { const url = req.nextUrl.clone(); url.protocol = 'https'; url.port = ''; url.hostname = 'www.qrmaster.net'; return NextResponse.redirect(url, 301); } // 301 Redirects for /guide -> /learn to avoid duplicate content and consolidate authority if (path === '/guide/tracking-analytics') { return attachAttributionCookie(req, NextResponse.redirect(new URL('/learn/tracking', req.url), 301)); } if (path === '/guide/bulk-qr-code-generation') { return attachAttributionCookie(req, NextResponse.redirect(new URL('/learn/developer', req.url), 301)); } if (path === '/guide/qr-code-best-practices') { return attachAttributionCookie(req, NextResponse.redirect(new URL('/learn/basics', req.url), 301)); } if (path === '/create-qr') { return attachAttributionCookie(req, NextResponse.redirect(new URL('/dynamic-qr-code-generator', req.url), 301)); } if (path === '/bar-code-generator' || path === '/barcode-generator') { return attachAttributionCookie(req, NextResponse.redirect(new URL('/tools/barcode-generator', req.url), 301)); } // Public routes that don't require authentication const publicPaths = [ '/', '/pricing', '/faq', '/blog', '/login', '/signup', '/privacy', '/newsletter', '/restaurants', '/tools', '/features', // '/guide', // Redirected to /learn/* '/de', '/qr-code-erstellen', '/dynamic-qr-code-generator', '/dynamic-barcode-generator', '/bulk-qr-code-generator', '/qr-code-tracking', '/qr-code-analytics', '/reprint-calculator', '/custom-qr-code-generator', '/manage-qr-codes', '/coupon', '/feedback', '/vcard', '/display', '/contact', '/about', '/learn', '/use-cases', '/authors', '/press', '/testimonials', '/qr-code-for-marketing-campaigns', '/qr-code-for', '/qr-code-print-size-guide', '/use-cases/flyer-qr-codes', '/use-cases/packaging-qr-codes', '/use-cases/real-estate-sign-qr-codes', '/use-cases/feedback-qr-codes', '/use-cases/payment-qr-codes', '/use-cases/coupon-qr-codes', '/alternatives', '/vs', ]; // Check if path is public const isPublicPath = publicPaths.some(p => path === p || path.startsWith(p + '/')); // Allow API routes if (path.startsWith('/api/')) { return attachAttributionCookie(req, NextResponse.next()); } // Allow redirect routes (QR code redirects) if (path.startsWith('/r/')) { return attachAttributionCookie(req, NextResponse.next()); } // Allow static files if (path.includes('.') || path.startsWith('/_next')) { return attachAttributionCookie(req, NextResponse.next()); } // Allow public paths if (isPublicPath) { return attachAttributionCookie(req, NextResponse.next()); } const protectedPaths = [ '/analytics', '/bulk-creation', '/create', '/dashboard', '/integrations', '/onboarding', '/qr', '/settings', ]; const isProtectedPath = protectedPaths.some(p => path === p || path.startsWith(p + '/')); if (!isProtectedPath) { return attachAttributionCookie(req, NextResponse.next()); } // For protected routes, require a validly signed userId cookie const userId = await verifySignedUserIdEdge(req.cookies.get('userId')?.value); if (!userId) { // Not authenticated - redirect to signup const signupUrl = new URL('/signup', req.url); const redirectTarget = `${path}${req.nextUrl.search}`; signupUrl.searchParams.set('redirect', redirectTarget); return attachAttributionCookie(req, NextResponse.redirect(signupUrl)); } // Authenticated - allow access return attachAttributionCookie(req, NextResponse.next()); } export const config = { matcher: [ /* * Match all request paths except for the ones starting with: * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - public folder */ '/((?!_next/static|_next/image|favicon.ico|logo.svg|og-image.png).*)', ], };