fix: ajustes de rota
Some checks failed
Deploy QR Rapido / test (push) Failing after 1m18s
Deploy QR Rapido / build-and-push (push) Has been skipped
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped

This commit is contained in:
Ricardo Carneiro 2026-02-15 21:05:57 -03:00
parent 72bbbeea4a
commit 7dccfc10f0
2 changed files with 59 additions and 94 deletions

View File

@ -299,7 +299,7 @@ namespace QRRapidoApp.Controllers
[Route("sitemap.xml")] [Route("sitemap.xml")]
public async Task<IActionResult> Sitemap() public async Task<IActionResult> Sitemap()
{ {
var baseUrl = "https://qrrapido.site"; var baseUrl = $"{Request.Scheme}://{Request.Host}";
var now = DateTime.UtcNow.ToString("yyyy-MM-dd"); var now = DateTime.UtcNow.ToString("yyyy-MM-dd");
var sitemapBuilder = new System.Text.StringBuilder(); var sitemapBuilder = new System.Text.StringBuilder();
@ -324,22 +324,15 @@ namespace QRRapidoApp.Controllers
} }
// Core entry points // Core entry points
// "/" is canonical for Portuguese, "/es-PY" for Spanish
AppendUrl("/", "daily", "1.0"); AppendUrl("/", "daily", "1.0");
AppendUrl("/es-PY", "daily", "0.9"); AppendUrl("/es-PY", "daily", "0.9");
// Tools (Landing Pages) - Portuguese without prefix, Spanish with /es-PY // Tools (Landing Pages)
var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "texto", "url" }; var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "texto", "url" };
// Portuguese tools (canonical, no prefix)
foreach (var tool in tools) foreach (var tool in tools)
{ {
AppendUrl($"/{tool}", "weekly", "0.9"); AppendUrl($"/{tool}", "weekly", "0.9");
}
// Spanish tools (with /es-PY prefix)
foreach (var tool in tools)
{
AppendUrl($"/es-PY/{tool}", "weekly", "0.8"); AppendUrl($"/es-PY/{tool}", "weekly", "0.8");
} }
@ -354,41 +347,34 @@ namespace QRRapidoApp.Controllers
new { Path = "Home/Terms", ChangeFreq = "monthly", Priority = "0.4" } new { Path = "Home/Terms", ChangeFreq = "monthly", Priority = "0.4" }
}; };
// Portuguese informational pages (no prefix)
foreach (var page in informationalPages) foreach (var page in informationalPages)
{ {
AppendUrl($"/{page.Path}", page.ChangeFreq, page.Priority); AppendUrl($"/{page.Path}", page.ChangeFreq, page.Priority);
}
// Spanish informational pages
foreach (var page in informationalPages)
{
AppendUrl($"/es-PY/{page.Path}", page.ChangeFreq, page.Priority); AppendUrl($"/es-PY/{page.Path}", page.ChangeFreq, page.Priority);
} }
// Dynamic tutorial pages // Dynamic tutorial pages from Markdown
try try
{ {
var allArticles = await _markdownService.GetAllArticlesForSitemapAsync(); var allArticles = await _markdownService.GetAllArticlesForSitemapAsync();
foreach (var article in allArticles) foreach (var article in allArticles)
{ {
// Use slug from metadata or generate from title if missing
var slug = !string.IsNullOrWhiteSpace(article.Slug) var slug = !string.IsNullOrWhiteSpace(article.Slug)
? article.Slug ? article.Slug
: article.Title.ToLower().Replace(" ", "-"); : article.Title.ToLower().Replace(" ", "-");
var encodedSlug = System.Uri.EscapeDataString(slug); // Ensure slug is only encoded once and special characters are handled
var lastMod = article.LastMod.ToString("yyyy-MM-dd"); var encodedSlug = slug; // Assuming slug is already URL-safe from filename
// pt-BR tutorials go under /tutoriais/, es-PY under /es-PY/tutoriais/
var tutorialPath = article.Culture == "pt-BR" var tutorialPath = article.Culture == "pt-BR"
? $"/tutoriais/{encodedSlug}" ? $"/tutoriais/{encodedSlug}"
: $"/{article.Culture}/tutoriais/{encodedSlug}"; : $"/es-PY/tutoriais/{encodedSlug}";
var lastMod = article.LastMod.ToString("yyyy-MM-dd");
AppendUrl(tutorialPath, "weekly", "0.8", lastMod); AppendUrl(tutorialPath, "weekly", "0.8", lastMod);
} }
_logger.LogInformation("Generated sitemap with {Count} tutorial articles", allArticles.Count);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -397,7 +383,7 @@ namespace QRRapidoApp.Controllers
sitemapBuilder.AppendLine("</urlset>"); sitemapBuilder.AppendLine("</urlset>");
return Content(sitemapBuilder.ToString(), "application/xml"); return Content(sitemapBuilder.ToString(), "application/xml", System.Text.Encoding.UTF8);
} }
} }

View File

@ -40,104 +40,87 @@ namespace QRRapidoApp.Middleware
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries); var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
var firstSegment = segments.Length > 0 ? segments[0] : ""; var firstSegment = segments.Length > 0 ? segments[0] : "";
// Check if URL starts with a culture prefix // Check if URL starts with a culture segment (supported or unsupported)
if (IsCultureSegment(firstSegment)) if (IsCultureLikeSegment(firstSegment))
{ {
// /pt-BR/* → Redirect 301 to /* (remove pt-BR prefix, it's the default) // Supported: pt-BR (Default/Canonical)
if (string.Equals(firstSegment, "pt-BR", StringComparison.OrdinalIgnoreCase)) if (string.Equals(firstSegment, "pt-BR", StringComparison.OrdinalIgnoreCase))
{ {
var remainingPath = segments.Length > 1 return RedirectToCanonical(context, segments);
? "/" + string.Join('/', segments.Skip(1))
: "/";
if (context.Request.QueryString.HasValue)
{
remainingPath += context.Request.QueryString.Value;
}
_logger.LogInformation("Redirecting /pt-BR{Path} to {RedirectUrl} (canonical)",
remainingPath == "/" ? "" : remainingPath, remainingPath);
context.Response.Redirect(remainingPath, permanent: true);
return;
} }
// /es-PY/* → Continue normally (Spanish has its own URLs) // Supported: es-PY
if (string.Equals(firstSegment, "es-PY", StringComparison.OrdinalIgnoreCase)) if (string.Equals(firstSegment, "es-PY", StringComparison.OrdinalIgnoreCase))
{ {
// Set culture for the request
SetCulture(context, "es-PY"); SetCulture(context, "es-PY");
await _next(context); await _next(context);
return; return;
} }
// Handle lowercase aliases: /pt-br → /pt-BR, /es-py → /es-PY // Handle aliases or unsupported cultures (e.g. /en/, /pt/, /es/)
if (TryHandleCultureAlias(context, firstSegment, segments)) if (TryHandleCultureAliasOrUnknown(context, firstSegment, segments))
{ {
return; return;
} }
} }
// No culture prefix → Serve in Portuguese (default) // No culture prefix or unknown segment -> Serve in Portuguese (default)
// URLs like /, /pix, /wifi, /vcard etc.
SetCulture(context, DefaultCulture); SetCulture(context, DefaultCulture);
await _next(context); await _next(context);
} }
private bool IsCultureSegment(string segment) private bool IsCultureLikeSegment(string segment)
{ {
if (string.IsNullOrEmpty(segment)) return false; if (string.IsNullOrEmpty(segment)) return false;
// Check exact matches and aliases // Matches xx (like en, pt, es) or xx-XX (like pt-BR, en-US)
return string.Equals(segment, "pt-BR", StringComparison.OrdinalIgnoreCase) || return segment.Length == 2 || (segment.Length == 5 && segment[2] == '-');
string.Equals(segment, "es-PY", StringComparison.OrdinalIgnoreCase) ||
string.Equals(segment, "pt", StringComparison.OrdinalIgnoreCase) ||
string.Equals(segment, "es", StringComparison.OrdinalIgnoreCase);
} }
private bool TryHandleCultureAlias(HttpContext context, string firstSegment, string[] segments) private bool TryHandleCultureAliasOrUnknown(HttpContext context, string firstSegment, string[] segments)
{ {
string? targetCulture = null; // Map known aliases to canonical forms
string? targetPrefix = null; if (string.Equals(firstSegment, "es", StringComparison.OrdinalIgnoreCase) ||
string.Equals(firstSegment, "es-py", StringComparison.OrdinalIgnoreCase))
// Map aliases to canonical forms
if (string.Equals(firstSegment, "pt", StringComparison.OrdinalIgnoreCase) ||
(string.Equals(firstSegment, "pt-br", StringComparison.Ordinal) && firstSegment != "pt-BR"))
{ {
// /pt/* or /pt-br/* → Redirect to /* (Portuguese is canonical without prefix) return RedirectToLanguage(context, "es-PY", segments);
targetCulture = "pt-BR";
targetPrefix = ""; // No prefix for Portuguese
}
else if (string.Equals(firstSegment, "es", StringComparison.OrdinalIgnoreCase) ||
(string.Equals(firstSegment, "es-py", StringComparison.OrdinalIgnoreCase) && firstSegment != "es-PY"))
{
// /es/* or /es-py/* → Redirect to /es-PY/*
targetCulture = "es-PY";
targetPrefix = "/es-PY";
} }
if (targetCulture != null) // For anything else that looks like a culture (en, pt, fr, etc.)
// but isn't explicitly es-PY, we redirect to the canonical (no prefix)
// This prevents 404s for /en/, /pt-BR/ (redirects to /), etc.
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)
{ {
var remainingPath = segments.Length > 1 url += context.Request.QueryString.Value;
? "/" + string.Join('/', segments.Skip(1))
: "";
var redirectUrl = targetPrefix + remainingPath;
if (string.IsNullOrEmpty(redirectUrl)) redirectUrl = "/";
if (context.Request.QueryString.HasValue)
{
redirectUrl += context.Request.QueryString.Value;
}
_logger.LogInformation("Redirecting alias '{Alias}' to canonical URL: {RedirectUrl}",
firstSegment, redirectUrl);
context.Response.Redirect(redirectUrl, permanent: true);
return true;
} }
return false; _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) private void SetCulture(HttpContext context, string culture)
@ -145,8 +128,6 @@ namespace QRRapidoApp.Middleware
var cultureInfo = new CultureInfo(culture); var cultureInfo = new CultureInfo(culture);
CultureInfo.CurrentCulture = cultureInfo; CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo; CultureInfo.CurrentUICulture = cultureInfo;
// Store in HttpContext for downstream use
context.Items["Culture"] = culture; context.Items["Culture"] = culture;
} }
@ -155,10 +136,8 @@ namespace QRRapidoApp.Middleware
var specialRoutes = new[] var specialRoutes = new[]
{ {
"api/", "health", "_framework/", "lib/", "css/", "js/", "images/", "api/", "health", "_framework/", "lib/", "css/", "js/", "images/",
"favicon.ico", "robots.txt", "sitemap.xml", "favicon.ico", "robots.txt", "sitemap.xml", "ads.txt",
"signin-microsoft", "signin-google", "signout-callback-oidc", "signin-", "signout-", "Account/", "Pagamento/", "Home/Error"
"Account/ExternalLoginCallback", "Account/Logout", "Pagamento/CreateCheckout",
"Pagamento/StripeWebhook", "api/QR", "Home/Error", "ads.txt"
}; };
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase)); return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));