using System.Globalization; using System.Linq; namespace QRRapidoApp.Middleware { /// /// Middleware de redirecionamento de idioma otimizado para SEO. /// /// Comportamento: /// - "/" → Retorna 200 OK em Português (canonical) /// - "/pt-BR" ou "/pt-BR/*" → Redireciona 301 para "/" ou "/*" (sem prefixo) /// - "/es" ou "/es/*" → Retorna 200 OK em Espanhol (mantém URL) /// - "/en" ou "/en/*" → Retorna 200 OK em Inglês (mantém URL) /// - "/pix", "/wifi", etc. → Retorna 200 OK em Português (sem redirect) /// public class LanguageRedirectionMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly string[] _supportedCultures = { "pt-BR", "es", "en" }; private const string DefaultCulture = "pt-BR"; private const string CookieName = ".AspNetCore.Culture"; public LanguageRedirectionMiddleware(RequestDelegate next, ILogger logger) { _next = next; _logger = logger; } public async Task InvokeAsync(HttpContext context) { var pathString = context.Request.Path.Value ?? ""; // Skip API routes immediately using the raw path if (pathString.StartsWith("/api", StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // Skip redirection for non-GET requests (POST/PUT/PATCH/DELETE). // A 301 redirect converts POST to GET, breaking form submissions. // Language canonicalization only matters for crawlers (GET). if (!HttpMethods.IsGet(context.Request.Method) && !HttpMethods.IsHead(context.Request.Method)) { await _next(context); return; } var path = pathString.TrimStart('/') ?? ""; // Skip other special routes if (IsSpecialRoute(path)) { await _next(context); return; } var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); var firstSegment = segments.Length > 0 ? segments[0] : ""; // Check if URL starts with a culture segment (supported or unsupported) if (IsCultureLikeSegment(firstSegment)) { // Supported: pt-BR (Default/Canonical) if (string.Equals(firstSegment, "pt-BR", StringComparison.OrdinalIgnoreCase)) { RedirectToCanonical(context, segments); return; } // Supported: es (Spanish neutral) if (string.Equals(firstSegment, "es", StringComparison.OrdinalIgnoreCase)) { SetCulture(context, "es"); await _next(context); return; } // Supported: en (English) if (string.Equals(firstSegment, "en", StringComparison.OrdinalIgnoreCase)) { SetCulture(context, "en"); await _next(context); return; } // Handle aliases or unsupported cultures (e.g. /en-US/, /pt/) if (TryHandleCultureAliasOrUnknown(context, firstSegment, segments)) { return; } } // No culture prefix or unknown segment -> Serve in Portuguese (default) SetCulture(context, DefaultCulture); await _next(context); } private bool IsCultureLikeSegment(string segment) { if (string.IsNullOrEmpty(segment)) return false; // Matches xx (like en, pt, es) or xx-XX (like pt-BR, en-US) return segment.Length == 2 || (segment.Length == 5 && segment[2] == '-'); } private bool TryHandleCultureAliasOrUnknown(HttpContext context, string firstSegment, string[] segments) { // Map known aliases to canonical forms if (string.Equals(firstSegment, "es-py", StringComparison.OrdinalIgnoreCase)) return RedirectToLanguage(context, "es", segments); if (string.Equals(firstSegment, "en-us", StringComparison.OrdinalIgnoreCase) || string.Equals(firstSegment, "en-gb", StringComparison.OrdinalIgnoreCase)) return RedirectToLanguage(context, "en", segments); // For anything else that looks like a culture (pt, fr, de, etc.) // redirect to the canonical Portuguese URL (no prefix). return RedirectToCanonical(context, segments); } private bool RedirectToLanguage(HttpContext context, string culture, string[] segments) { var remainingPath = segments.Length > 1 ? "/" + string.Join('/', segments.Skip(1)) : ""; var redirectUrl = "/" + culture + remainingPath; return ExecuteRedirect(context, redirectUrl); } private bool RedirectToCanonical(HttpContext context, string[] segments) { var remainingPath = segments.Length > 1 ? "/" + string.Join('/', segments.Skip(1)) : "/"; return ExecuteRedirect(context, remainingPath); } private bool ExecuteRedirect(HttpContext context, string url) { if (context.Request.QueryString.HasValue) { url += context.Request.QueryString.Value; } _logger.LogInformation("SEO Redirect: {Source} -> {Dest}", context.Request.Path, url); context.Response.Redirect(url, permanent: true); return true; } private void SetCulture(HttpContext context, string culture) { var cultureInfo = new CultureInfo(culture); CultureInfo.CurrentCulture = cultureInfo; CultureInfo.CurrentUICulture = cultureInfo; context.Items["Culture"] = culture; } private bool IsSpecialRoute(string path) { var specialRoutes = new[] { "api/", "health", "_framework/", "lib/", "css/", "js/", "images/", "favicon.ico", "robots.txt", "sitemap.xml", "ads.txt", "signin-", "signout-", "Account/", "Pagamento/", "Home/Error" }; return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase)); } } }