fix: PIX + idioma espanhol e SEO
All checks were successful
Deploy QR Rapido / test (push) Successful in 43s
Deploy QR Rapido / build-and-push (push) Successful in 16m43s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m55s

This commit is contained in:
Ricardo Carneiro 2026-01-25 12:04:24 -03:00
parent bdf78ed418
commit 162e28ae5a
10 changed files with 380 additions and 252 deletions

BIN
AoSelecionarES.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -33,8 +33,12 @@ namespace QRRapidoApp.Controllers
_markdownService = markdownService;
}
// Default fallback route handled by Program.cs map
// Home page routes
// "/" → Portuguese (canonical)
// "/es-PY" → Spanish
[HttpGet]
[Route("/")]
[Route("es-PY")]
public async Task<IActionResult> Index(string? qrType = null)
{
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
@ -106,37 +110,42 @@ namespace QRRapidoApp.Controllers
return View("Index");
}
// Dedicated SEO Routes - These act as virtual pages
// Dedicated SEO Routes - Landing pages for each QR type
// Portuguese (default): /pix, /wifi, etc. (no culture prefix)
// Spanish: /es-PY/pix, /es-PY/wifi, etc.
[Route("pix")]
[Route("{culture}/pix")]
[Route("es-PY/pix")]
public async Task<IActionResult> Pix() => await Index("pix");
[Route("wifi")]
[Route("{culture}/wifi")]
[Route("es-PY/wifi")]
public async Task<IActionResult> Wifi() => await Index("wifi");
[Route("vcard")]
[Route("{culture}/vcard")]
[Route("es-PY/vcard")]
public async Task<IActionResult> VCard() => await Index("vcard");
[Route("whatsapp")]
[Route("{culture}/whatsapp")]
[Route("es-PY/whatsapp")]
public async Task<IActionResult> WhatsApp() => await Index("whatsapp");
[Route("email")]
[Route("{culture}/email")]
[Route("es-PY/email")]
public async Task<IActionResult> Email() => await Index("email");
[Route("sms")]
[Route("{culture}/sms")]
[Route("es-PY/sms")]
public async Task<IActionResult> Sms() => await Index("sms");
[Route("texto")]
[Route("text")]
[Route("{culture}/text")]
[Route("es-PY/texto")]
[Route("es-PY/text")]
public async Task<IActionResult> Text() => await Index("text");
[Route("url")]
[Route("{culture}/url")]
[Route("es-PY/url")]
public async Task<IActionResult> UrlType() => await Index("url");
public IActionResult Privacy()
@ -315,43 +324,46 @@ namespace QRRapidoApp.Controllers
}
// Core entry points
// "/" is canonical for Portuguese, "/es-PY" for Spanish
AppendUrl("/", "daily", "1.0");
AppendUrl("/pt-BR", "daily", "0.9");
AppendUrl("/es-PY", "daily", "0.9");
// Tools (Virtual Pages)
var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "text", "url" };
var cultures = new[] { "pt-BR", "es-PY" };
// Tools (Landing Pages) - Portuguese without prefix, Spanish with /es-PY
var tools = new[] { "pix", "wifi", "vcard", "whatsapp", "email", "sms", "texto", "url" };
foreach (var culture in cultures)
{
// Portuguese tools (canonical, no prefix)
foreach (var tool in tools)
{
AppendUrl($"/{culture}/{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");
}
// Informational pages
var informationalPages = new[]
{
new { Path = "Home/About", ChangeFreq = "monthly", Priority = "0.8" },
new { Path = "Home/Contact", ChangeFreq = "monthly", Priority = "0.8" },
new { Path = "Home/FAQ", ChangeFreq = "weekly", Priority = "0.9" },
new { Path = "Home/About", ChangeFreq = "monthly", Priority = "0.7" },
new { Path = "Home/Contact", ChangeFreq = "monthly", Priority = "0.7" },
new { Path = "Home/FAQ", ChangeFreq = "weekly", Priority = "0.8" },
new { Path = "Home/HowToUse", ChangeFreq = "weekly", Priority = "0.8" },
new { Path = "Home/Privacy", ChangeFreq = "monthly", Priority = "0.5" },
new { Path = "Home/Terms", ChangeFreq = "monthly", Priority = "0.5" }
new { Path = "Home/Privacy", ChangeFreq = "monthly", Priority = "0.4" },
new { Path = "Home/Terms", ChangeFreq = "monthly", Priority = "0.4" }
};
// Portuguese informational pages (no prefix)
foreach (var page in informationalPages)
{
AppendUrl($"/{page.Path}", page.ChangeFreq, page.Priority);
}
foreach (var culture in cultures)
{
// Spanish informational pages
foreach (var page in informationalPages)
{
AppendUrl($"/{culture}/{page.Path}", page.ChangeFreq, page.Priority);
}
AppendUrl($"/es-PY/{page.Path}", page.ChangeFreq, page.Priority);
}
// Dynamic tutorial pages
@ -368,7 +380,12 @@ namespace QRRapidoApp.Controllers
var encodedSlug = System.Uri.EscapeDataString(slug);
var lastMod = article.LastMod.ToString("yyyy-MM-dd");
AppendUrl($"/{article.Culture}/tutoriais/{encodedSlug}", "weekly", "0.8", lastMod);
// pt-BR tutorials go under /tutoriais/, es-PY under /es-PY/tutoriais/
var tutorialPath = article.Culture == "pt-BR"
? $"/tutoriais/{encodedSlug}"
: $"/{article.Culture}/tutoriais/{encodedSlug}";
AppendUrl(tutorialPath, "weekly", "0.8", lastMod);
}
_logger.LogInformation("Generated sitemap with {Count} tutorial articles", allArticles.Count);

View File

@ -27,11 +27,19 @@ namespace QRRapidoApp.Controllers
_config = config;
}
[Route("{culture:regex(^(pt-BR|es-PY)$)}/tutoriais/{slug}")]
public async Task<IActionResult> Article(string slug, string culture)
// Portuguese: /tutoriais/{slug} (canonical, no prefix)
// Spanish: /es-PY/tutoriais/{slug}
[Route("tutoriais/{slug}")]
[Route("es-PY/tutoriais/{slug}")]
public async Task<IActionResult> Article(string slug, string? culture = null)
{
try
{
// Determine culture from URL path if not provided
culture ??= Request.Path.Value?.StartsWith("/es-PY", StringComparison.OrdinalIgnoreCase) == true
? "es-PY"
: "pt-BR";
var article = await _markdownService.GetArticleAsync(slug, culture);
if (article == null)
@ -77,11 +85,19 @@ namespace QRRapidoApp.Controllers
}
}
[Route("{culture:regex(^(pt-BR|es-PY)$)}/tutoriais")]
public async Task<IActionResult> Index(string culture)
// Portuguese: /tutoriais (canonical, no prefix)
// Spanish: /es-PY/tutoriais
[Route("tutoriais")]
[Route("es-PY/tutoriais")]
public async Task<IActionResult> Index(string? culture = null)
{
try
{
// Determine culture from URL path if not provided
culture ??= Request.Path.Value?.StartsWith("/es-PY", StringComparison.OrdinalIgnoreCase) == true
? "es-PY"
: "pt-BR";
var articles = await _markdownService.GetAllArticlesAsync(culture);
// Set ViewBag

View File

@ -3,19 +3,22 @@ using System.Linq;
namespace QRRapidoApp.Middleware
{
/// <summary>
/// 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-PY" ou "/es-PY/*" → Retorna 200 OK em Espanhol (mantém URL)
/// - "/pix", "/wifi", etc. → Retorna 200 OK em Português (sem redirect)
/// </summary>
public class LanguageRedirectionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LanguageRedirectionMiddleware> _logger;
private readonly string[] _supportedCultures = { "pt-BR", "es-PY" };
private readonly Dictionary<string, string> _cultureAliases = new(StringComparer.OrdinalIgnoreCase)
{
{ "pt", "pt-BR" },
{ "pt-br", "pt-BR" },
{ "es", "es-PY" },
{ "es-py", "es-PY" }
};
private const string DefaultCulture = "pt-BR";
private const string CookieName = ".AspNetCore.Culture";
public LanguageRedirectionMiddleware(RequestDelegate next, ILogger<LanguageRedirectionMiddleware> logger)
{
@ -27,84 +30,109 @@ namespace QRRapidoApp.Middleware
{
var path = context.Request.Path.Value?.TrimStart('/') ?? "";
if (TryHandleCultureAlias(context, path))
{
return;
}
if (HasCultureInPath(path) || IsSpecialRoute(path))
// Skip special routes (static files, API, auth callbacks, etc.)
if (IsSpecialRoute(path))
{
await _next(context);
return;
}
var detectedCulture = DetectBrowserLanguage(context);
// ALWAYS Redirect to include culture in path for consistency and SEO
// This ensures /pix becomes /pt-BR/pix or /es-PY/pix
var redirectUrl = $"/{detectedCulture}";
if (!string.IsNullOrEmpty(path))
{
redirectUrl += $"/{path}";
}
if (context.Request.QueryString.HasValue)
{
redirectUrl += context.Request.QueryString.Value;
}
_logger.LogInformation("Redirecting to localized URL: {RedirectUrl} (detected culture: {Culture})",
redirectUrl, detectedCulture);
context.Response.Redirect(redirectUrl, permanent: false);
}
private bool HasCultureInPath(string path)
{
if (string.IsNullOrEmpty(path))
return false;
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length == 0)
return false;
var firstSegment = segments.Length > 0 ? segments[0] : "";
return _supportedCultures.Contains(segments[0]);
}
private bool TryHandleCultureAlias(HttpContext context, string path)
// Check if URL starts with a culture prefix
if (IsCultureSegment(firstSegment))
{
if (string.IsNullOrEmpty(path))
// /pt-BR/* → Redirect 301 to /* (remove pt-BR prefix, it's the default)
if (string.Equals(firstSegment, "pt-BR", StringComparison.OrdinalIgnoreCase))
{
return false;
}
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length == 0)
{
return false;
}
var firstSegment = segments[0];
if (_cultureAliases.TryGetValue(firstSegment, out var mappedCulture))
{
// Don't redirect if already using the canonical culture (case-sensitive check)
if (firstSegment == mappedCulture)
{
return false;
}
var remainingSegments = segments.Length > 1
var remainingPath = segments.Length > 1
? "/" + string.Join('/', segments.Skip(1))
: string.Empty;
: "/";
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)
if (string.Equals(firstSegment, "es-PY", StringComparison.OrdinalIgnoreCase))
{
// Set culture for the request
SetCulture(context, "es-PY");
await _next(context);
return;
}
// Handle lowercase aliases: /pt-br → /pt-BR, /es-py → /es-PY
if (TryHandleCultureAlias(context, firstSegment, segments))
{
return;
}
}
// No culture prefix → Serve in Portuguese (default)
// URLs like /, /pix, /wifi, /vcard etc.
SetCulture(context, DefaultCulture);
await _next(context);
}
private bool IsCultureSegment(string segment)
{
if (string.IsNullOrEmpty(segment)) return false;
// Check exact matches and aliases
return string.Equals(segment, "pt-BR", StringComparison.OrdinalIgnoreCase) ||
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)
{
string? targetCulture = null;
string? targetPrefix = null;
// 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)
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)
{
var remainingPath = segments.Length > 1
? "/" + string.Join('/', segments.Skip(1))
: "";
var redirectUrl = targetPrefix + remainingPath;
if (string.IsNullOrEmpty(redirectUrl)) redirectUrl = "/";
var redirectUrl = $"/{mappedCulture}{remainingSegments}";
if (context.Request.QueryString.HasValue)
{
redirectUrl += context.Request.QueryString.Value;
}
_logger.LogInformation("Redirecting alias '{Alias}' to canonical culture URL: {RedirectUrl}", firstSegment, redirectUrl);
_logger.LogInformation("Redirecting alias '{Alias}' to canonical URL: {RedirectUrl}",
firstSegment, redirectUrl);
context.Response.Redirect(redirectUrl, permanent: true);
return true;
}
@ -112,6 +140,16 @@ namespace QRRapidoApp.Middleware
return false;
}
private void SetCulture(HttpContext context, string culture)
{
var cultureInfo = new CultureInfo(culture);
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
// Store in HttpContext for downstream use
context.Items["Culture"] = culture;
}
private bool IsSpecialRoute(string path)
{
var specialRoutes = new[]
@ -125,38 +163,5 @@ namespace QRRapidoApp.Middleware
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase));
}
private string DetectBrowserLanguage(HttpContext context)
{
var acceptLanguage = context.Request.GetTypedHeaders().AcceptLanguage;
if (acceptLanguage != null && acceptLanguage.Any())
{
foreach (var lang in acceptLanguage.OrderByDescending(x => x.Quality ?? 1.0))
{
var langCode = lang.Value.Value;
// Exact match for es-PY
if (string.Equals(langCode, "es-PY", StringComparison.OrdinalIgnoreCase))
{
return "es-PY";
}
// Generic 'es' maps to 'es-PY'
if (langCode.StartsWith("es-", StringComparison.OrdinalIgnoreCase) || string.Equals(langCode, "es", StringComparison.OrdinalIgnoreCase))
{
return "es-PY";
}
// Check for pt-BR
if (langCode.StartsWith("pt-", StringComparison.OrdinalIgnoreCase) || string.Equals(langCode, "pt", StringComparison.OrdinalIgnoreCase))
{
return "pt-BR";
}
}
}
return DefaultCulture;
}
}
}

View File

@ -6,6 +6,17 @@ namespace QRRapidoApp.Providers
{
public Task<ProviderCultureResult?> DetermineProviderCultureResult(HttpContext httpContext)
{
// First check if the middleware has already determined the culture (e.g. for static routes like /es-PY/pix)
if (httpContext.Items.TryGetValue("Culture", out var cultureItem) && cultureItem is string cultureFromMiddleware)
{
var supportedCultures = new[] { "pt-BR", "es-PY" };
if (supportedCultures.Contains(cultureFromMiddleware))
{
return Task.FromResult<ProviderCultureResult?>(new ProviderCultureResult(cultureFromMiddleware, cultureFromMiddleware));
}
}
// Fallback to route data (standard routing)
var routeValues = httpContext.GetRouteData()?.Values;
if (routeValues != null && routeValues.TryGetValue("culture", out var cultureValue))
{

View File

@ -2150,4 +2150,16 @@
<data name="FAQ_StaticQRNote" xml:space="preserve">
<value>Los códigos QR estáticos son ideales para información permanente como WiFi, tarjetas de visita, URLs de sitios web y contactos.</value>
</data>
<data name="PixGeneratorTitle" xml:space="preserve">
<value>Generador de PIX</value>
</data>
<data name="PixGeneratorDesc" xml:space="preserve">
<value>Cree códigos QR para recibir pagos instantáneos. Compatible con todos los bancos brasileños.</value>
</data>
<data name="SellManyProductsTitle" xml:space="preserve">
<value>¿Vende muchos productos?</value>
</data>
<data name="SellManyProductsDesc" xml:space="preserve">
<value>Suscríbase a nuestro plan mensual y gestione códigos QR exclusivos para cada producto de su catálogo.</value>
</data>
</root>

View File

@ -2303,4 +2303,16 @@
<data name="FAQ_StaticQRNote" xml:space="preserve">
<value>QR codes estáticos são ideais para informações permanentes como WiFi, cartões de visita, URLs de sites, e contatos.</value>
</data>
<data name="PixGeneratorTitle" xml:space="preserve">
<value>Gerador de PIX</value>
</data>
<data name="PixGeneratorDesc" xml:space="preserve">
<value>Crie QR Codes para receber pagamentos instantâneos. Compatível com todos os bancos brasileiros.</value>
</data>
<data name="SellManyProductsTitle" xml:space="preserve">
<value>Vende muitos produtos?</value>
</data>
<data name="SellManyProductsDesc" xml:space="preserve">
<value>Assine nosso plano mensal e gerencie QR Codes exclusivos para cada produto do seu catálogo.</value>
</data>
</root>

View File

@ -7,6 +7,34 @@
ViewData["Title"] = "Home";
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
Layout = "~/Views/Shared/_Layout.cshtml";
// Server-Side Rendering: Determine which accordion to open based on qrType
var selectedQRType = (ViewBag.SelectedQRType as string)?.ToLowerInvariant() ?? "";
// Map qrType to accordion ID suffix (e.g., "pix" -> "pixQR")
var accordionIdMap = new Dictionary<string, string>
{
{ "url", "url" },
{ "text", "text" },
{ "texto", "text" },
{ "whatsapp", "whatsapp" },
{ "email", "email" },
{ "pix", "pix" },
{ "sms", "sms" },
{ "wifi", "wifi" },
{ "vcard", "vcard" }
};
// Helper function to determine if this accordion should be open
string GetAccordionClass(string accordionType) =>
accordionIdMap.TryGetValue(selectedQRType, out var mapped) && mapped == accordionType
? "accordion-collapse collapse show"
: "accordion-collapse collapse";
string GetButtonClass(string accordionType) =>
accordionIdMap.TryGetValue(selectedQRType, out var mapped) && mapped == accordionType
? "accordion-button"
: "accordion-button collapsed";
}
@ -512,8 +540,8 @@
<div class="d-flex">
<div class="me-3 display-4"><i class="fas fa-qrcode"></i></div>
<div>
<h5 class="alert-heading fw-bold">Gerador de PIX</h5>
<p class="mb-0">Crie QR Codes para receber pagamentos instantâneos. Compatível com todos os bancos brasileiros.</p>
<h5 class="alert-heading fw-bold">@Localizer["PixGeneratorTitle"]</h5>
<p class="mb-0">@Localizer["PixGeneratorDesc"]</p>
</div>
</div>
</div>
@ -521,8 +549,8 @@
<div class="alert alert-warning border-warning d-flex align-items-center mb-3">
<i class="fas fa-store fa-2x me-3 text-warning"></i>
<div>
<strong>Vende muitos produtos?</strong>
<p class="mb-0 small">Assine nosso <a href="/Pagamento/SelecaoPlano" class="alert-link">plano mensal</a> e gerencie QR Codes exclusivos para cada produto do seu catálogo.</p>
<strong>@Localizer["SellManyProductsTitle"]</strong>
<p class="mb-0 small">Assine nosso plano mensal e gerencie QR Codes exclusivos para cada produto do seu catálogo.</p>
</div>
</div>
@ -867,12 +895,12 @@
<!-- URL/Link QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#urlQR">
<button class="@GetButtonClass("url")" type="button" data-bs-toggle="collapse" data-bs-target="#urlQR">
<i class="fas fa-link text-primary me-2"></i>
<strong>@Localizer["URLLink"]</strong>
</button>
</h2>
<div id="urlQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="urlQR" class="@GetAccordionClass("url")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
@ -910,12 +938,12 @@
<!-- Text QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#textQR">
<button class="@GetButtonClass("text")" type="button" data-bs-toggle="collapse" data-bs-target="#textQR">
<i class="fas fa-align-left text-primary me-2"></i>
<strong>@Localizer["SimpleText"]</strong>
</button>
</h2>
<div id="textQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="textQR" class="@GetAccordionClass("text")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
@ -951,12 +979,12 @@
<!-- WhatsApp QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#whatsappQR">
<button class="@GetButtonClass("whatsapp")" type="button" data-bs-toggle="collapse" data-bs-target="#whatsappQR">
<i class="fab fa-whatsapp text-success me-2"></i>
<strong>WhatsApp</strong>
</button>
</h2>
<div id="whatsappQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="whatsappQR" class="@GetAccordionClass("whatsapp")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
@ -991,12 +1019,12 @@
<!-- Email QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#emailQR">
<button class="@GetButtonClass("email")" type="button" data-bs-toggle="collapse" data-bs-target="#emailQR">
<i class="fas fa-envelope text-primary me-2"></i>
<strong>@Localizer["Email"]</strong>
</button>
</h2>
<div id="emailQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="emailQR" class="@GetAccordionClass("email")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
@ -1031,12 +1059,12 @@
<!-- PIX QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#pixQR">
<button class="@GetButtonClass("pix")" type="button" data-bs-toggle="collapse" data-bs-target="#pixQR">
<i class="fas fa-qrcode text-success me-2"></i>
<strong>@Localizer["PIX"]</strong>
</button>
</h2>
<div id="pixQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="pixQR" class="@GetAccordionClass("pix")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
@ -1070,12 +1098,12 @@
<!-- SMS QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#smsQR">
<button class="@GetButtonClass("sms")" type="button" data-bs-toggle="collapse" data-bs-target="#smsQR">
<i class="fas fa-sms text-primary me-2"></i>
<strong>@Localizer["SMS"]</strong>
</button>
</h2>
<div id="smsQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="smsQR" class="@GetAccordionClass("sms")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
@ -1110,12 +1138,12 @@
<!-- WiFi QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#wifiQR">
<button class="@GetButtonClass("wifi")" type="button" data-bs-toggle="collapse" data-bs-target="#wifiQR">
<i class="fas fa-wifi text-primary me-2"></i>
<strong>@Localizer["WiFi"]</strong>
</button>
</h2>
<div id="wifiQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="wifiQR" class="@GetAccordionClass("wifi")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">
@ -1151,12 +1179,12 @@
<!-- vCard QR Code -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#vcardQR">
<button class="@GetButtonClass("vcard")" type="button" data-bs-toggle="collapse" data-bs-target="#vcardQR">
<i class="fas fa-address-card text-primary me-2"></i>
<strong>@Localizer["VCard"]</strong>
</button>
</h2>
<div id="vcardQR" class="accordion-collapse collapse" data-bs-parent="#qrTypesAccordion">
<div id="vcardQR" class="@GetAccordionClass("vcard")" data-bs-parent="#qrTypesAccordion">
<div class="accordion-body">
<div class="row">
<div class="col-md-6">

View File

@ -18,6 +18,34 @@
var appEnvironment = Configuration["App:Environment"] ?? HostEnvironment?.EnvironmentName ?? "Unknown";
var secretsLoaded = Configuration.GetValue<bool>("App:SecretsLoaded");
// SEO: Determine if current page is Spanish (for URL building)
var requestPath = Context.Request.Path.Value ?? "/";
var isSpanish = requestPath.StartsWith("/es-PY", StringComparison.OrdinalIgnoreCase);
// Get path without culture prefix for building alternate URLs
var pathWithoutCulture = requestPath;
if (isSpanish && requestPath.Length > 6)
{
pathWithoutCulture = requestPath.Substring(6); // Remove "/es-PY"
}
else if (isSpanish)
{
pathWithoutCulture = "/";
}
if (string.IsNullOrEmpty(pathWithoutCulture)) pathWithoutCulture = "/";
// Canonical URL - for Portuguese it's without prefix, for Spanish it's with /es-PY
var canonicalUrl = isSpanish
? $"https://qrrapido.site/es-PY{(pathWithoutCulture == "/" ? "" : pathWithoutCulture)}"
: $"https://qrrapido.site{pathWithoutCulture}";
// Alternate URLs for hreflang
var ptUrl = $"https://qrrapido.site{pathWithoutCulture}";
var esUrl = $"https://qrrapido.site/es-PY{(pathWithoutCulture == "/" ? "" : pathWithoutCulture)}";
// Culture prefix for internal links
var culturePrefix = isSpanish ? "/es-PY" : "";
if (User?.Identity?.IsAuthenticated == true)
{
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
@ -47,27 +75,27 @@
<meta http-equiv="Expires" content="0">
<!-- SEO Meta Tags -->
<meta name="description" content="@Localizer["QRGenerateDescription"]">
<meta name="keywords" content="qr rapido, gerador qr rapido, qr code rapido, codigo qr rapido, qr gratis rapido, generador qr rapido, qr ultrarapido">
<meta name="description" content="@(ViewBag.Description ?? Localizer["QRGenerateDescription"])">
<meta name="keywords" content="@(ViewBag.Keywords ?? "qr rapido, gerador qr rapido, qr code rapido, codigo qr rapido, qr gratis rapido, generador qr rapido, qr ultrarapido")">
<meta name="author" content="QR Rapido">
<meta name="robots" content="index, follow">
<!-- Canonical URL -->
<link rel="canonical" href="@Context.Request.GetDisplayUrl()">
<link rel="canonical" href="@canonicalUrl">
<!-- Hreflang for multilingual -->
<link rel="alternate" hreflang="pt-BR" href="https://qrrapido.site/pt-BR/">
<link rel="alternate" hreflang="es-PY" href="https://qrrapido.site/es-PY/">
<link rel="alternate" hreflang="x-default" href="https://qrrapido.site/">
<!-- Hreflang for multilingual SEO -->
<link rel="alternate" hreflang="pt-BR" href="@ptUrl">
<link rel="alternate" hreflang="es-PY" href="@esUrl">
<link rel="alternate" hreflang="x-default" href="@ptUrl">
<!-- Open Graph -->
<meta property="og:title" content="QR Rapido - @Localizer["FastestQRGeneratorWeb"]">
<meta property="og:description" content="@Localizer["QRGenerateDescription"]">
<meta property="og:title" content="@(ViewBag.Title ?? "QR Rapido") - @Localizer["FastestQRGeneratorWeb"]">
<meta property="og:description" content="@(ViewBag.Description ?? Localizer["QRGenerateDescription"])">
<meta property="og:image" content="https://qrrapido.site/images/qrrapido-og-image.png">
<meta property="og:url" content="@Context.Request.GetDisplayUrl()">
<meta property="og:url" content="@canonicalUrl">
<meta property="og:type" content="website">
<meta property="og:site_name" content="QR Rapido">
<meta property="og:locale" content="pt_BR">
<meta property="og:locale" content="@(isSpanish ? "es_PY" : "pt_BR")">
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image">
@ -378,11 +406,8 @@
<p class="mb-0 opacity-75">
@Localizer["AverageTimePrefix"] <strong>@Localizer["AverageTimeValue"]</strong> @Localizer["AverageTimeSuffix"]
</p>
@{
var currentCulture = System.Globalization.CultureInfo.CurrentUICulture.Name;
}
<div class="mt-3">
<a href="/@currentCulture/tutoriais" class="btn btn-sm btn-light">
<a href="@(culturePrefix)/tutoriais" class="btn btn-sm btn-light">
<i class="fas fa-graduation-cap"></i> @Localizer["ViewTutorials"]
</a>
</div>
@ -400,19 +425,31 @@
<footer class="bg-dark text-light py-4 mt-5">
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="col-md-5">
<h5>QR Rapido</h5>
<p class="small">@Localizer["FastestQRGeneratorDescription"]</p>
</div>
<div class="col-md-2">
<div class="col-md-3">
<h6>@Localizer["Tools"]</h6>
<div class="row">
<div class="col-6">
<ul class="list-unstyled small">
<li><a href="/pix" class="text-light text-decoration-none">Gerador de PIX</a></li>
<li><a href="/wifi" class="text-light text-decoration-none">QR Code WiFi</a></li>
<li><a href="/whatsapp" class="text-light text-decoration-none">Link WhatsApp</a></li>
<li><a href="/vcard" class="text-light text-decoration-none">Cartão Digital</a></li>
<li><a href="@(culturePrefix)/pix" class="text-light text-decoration-none">@Localizer["PixGenerator"]</a></li>
<li><a href="@(culturePrefix)/wifi" class="text-light text-decoration-none">QR Code WiFi</a></li>
<li><a href="@(culturePrefix)/whatsapp" class="text-light text-decoration-none">Link WhatsApp</a></li>
<li><a href="@(culturePrefix)/vcard" class="text-light text-decoration-none">@Localizer["DigitalCard"]</a></li>
</ul>
</div>
<div class="col-6">
<ul class="list-unstyled small">
<li><a href="@(culturePrefix)/email" class="text-light text-decoration-none">QR Code Email</a></li>
<li><a href="@(culturePrefix)/sms" class="text-light text-decoration-none">QR Code SMS</a></li>
<li><a href="@(culturePrefix)/texto" class="text-light text-decoration-none">@Localizer["TextQR"]</a></li>
<li><a href="@(culturePrefix)/url" class="text-light text-decoration-none">QR Code URL</a></li>
</ul>
</div>
</div>
</div>
<div class="col-md-2">
<h6>@Localizer["UsefulLinks"]</h6>
<ul class="list-unstyled small">

View File

@ -1,33 +1,30 @@
// Language switching functionality for QR Rapido
// SEO Strategy:
// - Portuguese (pt-BR): URLs without prefix (/, /pix, /wifi, etc.) - canonical
// - Spanish (es-PY): URLs with /es-PY prefix (/es-PY, /es-PY/pix, etc.)
document.addEventListener('DOMContentLoaded', function () {
// FORCE: Respect the URL culture above all else
let currentCulture = getCurrentCulture();
console.log('Current culture:', currentCulture);
localStorage.setItem('preferredLanguage', currentCulture);
const languageDropdownItems = document.querySelectorAll('.dropdown-item[data-lang]');
const currentLangSpan = document.getElementById('current-lang');
// Get current culture from URL or default to pt-BR
// Get current culture from URL
function getCurrentCulture() {
const pathSegments = window.location.pathname.split('/').filter(segment => segment);
const supportedCultures = ['pt-BR', 'es-PY', 'es'];
if (pathSegments.length > 0 && supportedCultures.includes(pathSegments[0])) {
return pathSegments[0];
// Check if first segment is es-PY (Spanish)
if (pathSegments.length > 0 && pathSegments[0].toLowerCase() === 'es-py') {
return 'es-PY';
}
// Default is Portuguese (no prefix in URL)
return 'pt-BR';
}
// Update current language display
// Update current language display in header
function updateCurrentLanguageDisplay(culture) {
const langMap = {
'pt-BR': 'PT',
'es-PY': 'ES',
'es': 'ES'
'es-PY': 'ES'
};
if (currentLangSpan) {
@ -41,40 +38,60 @@ document.addEventListener('DOMContentLoaded', function () {
const queryString = window.location.search;
const hash = window.location.hash;
// Remove existing culture from path if present
const pathSegments = currentPath.split('/').filter(segment => segment);
const supportedCultures = ['pt-BR', 'es-PY', 'es'];
// Get path segments, removing any culture prefix
let pathSegments = currentPath.split('/').filter(segment => segment);
// Remove current culture if it's the first segment
if (pathSegments.length > 0 && supportedCultures.includes(pathSegments[0])) {
// Remove existing culture prefix if present (es-PY or pt-BR)
if (pathSegments.length > 0) {
const firstSegment = pathSegments[0].toLowerCase();
if (firstSegment === 'es-py' || firstSegment === 'pt-br') {
pathSegments.shift();
}
}
// Build new path with selected culture
const newPath = '/' + newCulture + (pathSegments.length > 0 ? '/' + pathSegments.join('/') : '');
// Build new path based on selected culture
let newPath;
if (newCulture === 'pt-BR') {
// Portuguese: no prefix (canonical URLs)
newPath = pathSegments.length > 0 ? '/' + pathSegments.join('/') : '/';
} else {
// Spanish: add /es-PY prefix
newPath = '/es-PY' + (pathSegments.length > 0 ? '/' + pathSegments.join('/') : '');
}
return newPath + queryString + hash;
}
// Handle language selection
// Set culture cookie
function setCultureCookie(culture) {
// ASP.NET Core culture cookie format
const cookieValue = `c=${culture}|uic=${culture}`;
document.cookie = `.AspNetCore.Culture=${encodeURIComponent(cookieValue)}; path=/; max-age=31536000; SameSite=Lax`;
// Also store in localStorage for quick access
localStorage.setItem('preferredLanguage', culture);
}
// Handle language selection
languageDropdownItems.forEach(item => {
item.addEventListener('click', function (e) {
e.preventDefault();
const selectedLang = this.getAttribute('data-lang');
currentCulture = getCurrentCulture();
const currentCulture = getCurrentCulture();
// Don't do anything if same language selected
if (selectedLang === currentCulture) {
return;
}
// Track language change for analytics
if (typeof window.trackLanguageChange === 'function') {
window.trackLanguageChange(currentCulture, selectedLang);
}
// Store language preference in localStorage
localStorage.setItem('preferredLanguage', selectedLang);
// Set culture cookie for server-side processing
document.cookie = `culture=${selectedLang}; path=/; max-age=31536000; SameSite=Lax`;
// Save preference
setCultureCookie(selectedLang);
// Navigate to new URL with selected language
const newUrl = buildLocalizedUrl(selectedLang);
@ -82,46 +99,19 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
// Initialize current language display
currentCulture = getCurrentCulture();
// Initialize
const currentCulture = getCurrentCulture();
updateCurrentLanguageDisplay(currentCulture);
// Store current culture in localStorage if not already set
if (!localStorage.getItem('preferredLanguage')) {
localStorage.setItem('preferredLanguage', currentCulture);
}
// Ensure cookie matches URL culture
setCultureCookie(currentCulture);
});
// Utility function to get user's preferred language
// Utility function to get user's preferred language (for external use)
function getUserPreferredLanguage() {
// Check localStorage first
const storedLang = localStorage.getItem('preferredLanguage');
if (storedLang) {
return storedLang;
}
// Check browser language
const browserLang = navigator.language || navigator.userLanguage;
// Map browser languages to supported cultures
const langMap = {
'pt': 'pt-BR',
'pt-BR': 'pt-BR',
'es': 'es-PY',
'es-PY': 'es-PY'
};
// Check exact match first
if (langMap[browserLang]) {
return langMap[browserLang];
}
// Check language part only (e.g., 'es' from 'es-AR')
const langPart = browserLang.split('-')[0];
if (langMap[langPart]) {
return langMap[langPart];
}
return 'pt-BR'; // Default
}