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

View File

@ -27,11 +27,19 @@ namespace QRRapidoApp.Controllers
_config = config; _config = config;
} }
[Route("{culture:regex(^(pt-BR|es-PY)$)}/tutoriais/{slug}")] // Portuguese: /tutoriais/{slug} (canonical, no prefix)
public async Task<IActionResult> Article(string slug, string culture) // Spanish: /es-PY/tutoriais/{slug}
[Route("tutoriais/{slug}")]
[Route("es-PY/tutoriais/{slug}")]
public async Task<IActionResult> Article(string slug, string? culture = null)
{ {
try 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); var article = await _markdownService.GetArticleAsync(slug, culture);
if (article == null) if (article == null)
@ -77,11 +85,19 @@ namespace QRRapidoApp.Controllers
} }
} }
[Route("{culture:regex(^(pt-BR|es-PY)$)}/tutoriais")] // Portuguese: /tutoriais (canonical, no prefix)
public async Task<IActionResult> Index(string culture) // Spanish: /es-PY/tutoriais
[Route("tutoriais")]
[Route("es-PY/tutoriais")]
public async Task<IActionResult> Index(string? culture = null)
{ {
try 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); var articles = await _markdownService.GetAllArticlesAsync(culture);
// Set ViewBag // Set ViewBag

View File

@ -3,19 +3,22 @@ using System.Linq;
namespace QRRapidoApp.Middleware 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 public class LanguageRedirectionMiddleware
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly ILogger<LanguageRedirectionMiddleware> _logger; private readonly ILogger<LanguageRedirectionMiddleware> _logger;
private readonly string[] _supportedCultures = { "pt-BR", "es-PY" }; 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 DefaultCulture = "pt-BR";
private const string CookieName = ".AspNetCore.Culture";
public LanguageRedirectionMiddleware(RequestDelegate next, ILogger<LanguageRedirectionMiddleware> logger) public LanguageRedirectionMiddleware(RequestDelegate next, ILogger<LanguageRedirectionMiddleware> logger)
{ {
@ -27,84 +30,109 @@ namespace QRRapidoApp.Middleware
{ {
var path = context.Request.Path.Value?.TrimStart('/') ?? ""; var path = context.Request.Path.Value?.TrimStart('/') ?? "";
if (TryHandleCultureAlias(context, path)) // Skip special routes (static files, API, auth callbacks, etc.)
{ if (IsSpecialRoute(path))
return;
}
if (HasCultureInPath(path) || IsSpecialRoute(path))
{ {
await _next(context); await _next(context);
return; 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); var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length == 0) var firstSegment = segments.Length > 0 ? segments[0] : "";
return false;
return _supportedCultures.Contains(segments[0]); // Check if URL starts with a culture prefix
} if (IsCultureSegment(firstSegment))
private bool TryHandleCultureAlias(HttpContext context, string path)
{
if (string.IsNullOrEmpty(path))
{ {
return false; // /pt-BR/* → Redirect 301 to /* (remove pt-BR prefix, it's the default)
} if (string.Equals(firstSegment, "pt-BR", StringComparison.OrdinalIgnoreCase))
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 remainingPath = segments.Length > 1
? "/" + 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;
} }
var remainingSegments = segments.Length > 1 // /es-PY/* → Continue normally (Spanish has its own URLs)
? "/" + string.Join('/', segments.Skip(1)) if (string.Equals(firstSegment, "es-PY", StringComparison.OrdinalIgnoreCase))
: string.Empty; {
// 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) if (context.Request.QueryString.HasValue)
{ {
redirectUrl += context.Request.QueryString.Value; 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); context.Response.Redirect(redirectUrl, permanent: true);
return true; return true;
} }
@ -112,6 +140,16 @@ namespace QRRapidoApp.Middleware
return false; 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) private bool IsSpecialRoute(string path)
{ {
var specialRoutes = new[] var specialRoutes = new[]
@ -125,38 +163,5 @@ namespace QRRapidoApp.Middleware
return specialRoutes.Any(route => path.StartsWith(route, StringComparison.OrdinalIgnoreCase)); 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) 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; var routeValues = httpContext.GetRouteData()?.Values;
if (routeValues != null && routeValues.TryGetValue("culture", out var cultureValue)) if (routeValues != null && routeValues.TryGetValue("culture", out var cultureValue))
{ {

View File

@ -2150,4 +2150,16 @@
<data name="FAQ_StaticQRNote" xml:space="preserve"> <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> <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>
<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> </root>

View File

@ -2303,4 +2303,16 @@
<data name="FAQ_StaticQRNote" xml:space="preserve"> <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> <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>
<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> </root>

View File

@ -7,6 +7,34 @@
ViewData["Title"] = "Home"; ViewData["Title"] = "Home";
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
Layout = "~/Views/Shared/_Layout.cshtml"; 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="d-flex">
<div class="me-3 display-4"><i class="fas fa-qrcode"></i></div> <div class="me-3 display-4"><i class="fas fa-qrcode"></i></div>
<div> <div>
<h5 class="alert-heading fw-bold">Gerador de PIX</h5> <h5 class="alert-heading fw-bold">@Localizer["PixGeneratorTitle"]</h5>
<p class="mb-0">Crie QR Codes para receber pagamentos instantâneos. Compatível com todos os bancos brasileiros.</p> <p class="mb-0">@Localizer["PixGeneratorDesc"]</p>
</div> </div>
</div> </div>
</div> </div>
@ -521,8 +549,8 @@
<div class="alert alert-warning border-warning d-flex align-items-center mb-3"> <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> <i class="fas fa-store fa-2x me-3 text-warning"></i>
<div> <div>
<strong>Vende muitos produtos?</strong> <strong>@Localizer["SellManyProductsTitle"]</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> <p class="mb-0 small">Assine nosso plano mensal e gerencie QR Codes exclusivos para cada produto do seu catálogo.</p>
</div> </div>
</div> </div>
@ -867,12 +895,12 @@
<!-- URL/Link QR Code --> <!-- URL/Link QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fas fa-link text-primary me-2"></i>
<strong>@Localizer["URLLink"]</strong> <strong>@Localizer["URLLink"]</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -910,12 +938,12 @@
<!-- Text QR Code --> <!-- Text QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fas fa-align-left text-primary me-2"></i>
<strong>@Localizer["SimpleText"]</strong> <strong>@Localizer["SimpleText"]</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -951,12 +979,12 @@
<!-- WhatsApp QR Code --> <!-- WhatsApp QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fab fa-whatsapp text-success me-2"></i>
<strong>WhatsApp</strong> <strong>WhatsApp</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -991,12 +1019,12 @@
<!-- Email QR Code --> <!-- Email QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fas fa-envelope text-primary me-2"></i>
<strong>@Localizer["Email"]</strong> <strong>@Localizer["Email"]</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -1031,12 +1059,12 @@
<!-- PIX QR Code --> <!-- PIX QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fas fa-qrcode text-success me-2"></i>
<strong>@Localizer["PIX"]</strong> <strong>@Localizer["PIX"]</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -1070,12 +1098,12 @@
<!-- SMS QR Code --> <!-- SMS QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fas fa-sms text-primary me-2"></i>
<strong>@Localizer["SMS"]</strong> <strong>@Localizer["SMS"]</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -1110,12 +1138,12 @@
<!-- WiFi QR Code --> <!-- WiFi QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fas fa-wifi text-primary me-2"></i>
<strong>@Localizer["WiFi"]</strong> <strong>@Localizer["WiFi"]</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@ -1151,12 +1179,12 @@
<!-- vCard QR Code --> <!-- vCard QR Code -->
<div class="accordion-item"> <div class="accordion-item">
<h2 class="accordion-header"> <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> <i class="fas fa-address-card text-primary me-2"></i>
<strong>@Localizer["VCard"]</strong> <strong>@Localizer["VCard"]</strong>
</button> </button>
</h2> </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="accordion-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">

View File

@ -18,6 +18,34 @@
var appEnvironment = Configuration["App:Environment"] ?? HostEnvironment?.EnvironmentName ?? "Unknown"; var appEnvironment = Configuration["App:Environment"] ?? HostEnvironment?.EnvironmentName ?? "Unknown";
var secretsLoaded = Configuration.GetValue<bool>("App:SecretsLoaded"); 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) if (User?.Identity?.IsAuthenticated == true)
{ {
var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
@ -47,27 +75,27 @@
<meta http-equiv="Expires" content="0"> <meta http-equiv="Expires" content="0">
<!-- SEO Meta Tags --> <!-- SEO Meta Tags -->
<meta name="description" content="@Localizer["QRGenerateDescription"]"> <meta name="description" content="@(ViewBag.Description ?? 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="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="author" content="QR Rapido">
<meta name="robots" content="index, follow"> <meta name="robots" content="index, follow">
<!-- Canonical URL --> <!-- Canonical URL -->
<link rel="canonical" href="@Context.Request.GetDisplayUrl()"> <link rel="canonical" href="@canonicalUrl">
<!-- Hreflang for multilingual --> <!-- Hreflang for multilingual SEO -->
<link rel="alternate" hreflang="pt-BR" href="https://qrrapido.site/pt-BR/"> <link rel="alternate" hreflang="pt-BR" href="@ptUrl">
<link rel="alternate" hreflang="es-PY" href="https://qrrapido.site/es-PY/"> <link rel="alternate" hreflang="es-PY" href="@esUrl">
<link rel="alternate" hreflang="x-default" href="https://qrrapido.site/"> <link rel="alternate" hreflang="x-default" href="@ptUrl">
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:title" content="QR Rapido - @Localizer["FastestQRGeneratorWeb"]"> <meta property="og:title" content="@(ViewBag.Title ?? "QR Rapido") - @Localizer["FastestQRGeneratorWeb"]">
<meta property="og:description" content="@Localizer["QRGenerateDescription"]"> <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: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:type" content="website">
<meta property="og:site_name" content="QR Rapido"> <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 --> <!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
@ -378,11 +406,8 @@
<p class="mb-0 opacity-75"> <p class="mb-0 opacity-75">
@Localizer["AverageTimePrefix"] <strong>@Localizer["AverageTimeValue"]</strong> @Localizer["AverageTimeSuffix"] @Localizer["AverageTimePrefix"] <strong>@Localizer["AverageTimeValue"]</strong> @Localizer["AverageTimeSuffix"]
</p> </p>
@{
var currentCulture = System.Globalization.CultureInfo.CurrentUICulture.Name;
}
<div class="mt-3"> <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"] <i class="fas fa-graduation-cap"></i> @Localizer["ViewTutorials"]
</a> </a>
</div> </div>
@ -400,18 +425,30 @@
<footer class="bg-dark text-light py-4 mt-5"> <footer class="bg-dark text-light py-4 mt-5">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-5">
<h5>QR Rapido</h5> <h5>QR Rapido</h5>
<p class="small">@Localizer["FastestQRGeneratorDescription"]</p> <p class="small">@Localizer["FastestQRGeneratorDescription"]</p>
</div> </div>
<div class="col-md-2"> <div class="col-md-3">
<h6>@Localizer["Tools"]</h6> <h6>@Localizer["Tools"]</h6>
<ul class="list-unstyled small"> <div class="row">
<li><a href="/pix" class="text-light text-decoration-none">Gerador de PIX</a></li> <div class="col-6">
<li><a href="/wifi" class="text-light text-decoration-none">QR Code WiFi</a></li> <ul class="list-unstyled small">
<li><a href="/whatsapp" class="text-light text-decoration-none">Link WhatsApp</a></li> <li><a href="@(culturePrefix)/pix" class="text-light text-decoration-none">@Localizer["PixGenerator"]</a></li>
<li><a href="/vcard" class="text-light text-decoration-none">Cartão Digital</a></li> <li><a href="@(culturePrefix)/wifi" class="text-light text-decoration-none">QR Code WiFi</a></li>
</ul> <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>
<div class="col-md-2"> <div class="col-md-2">
<h6>@Localizer["UsefulLinks"]</h6> <h6>@Localizer["UsefulLinks"]</h6>

View File

@ -1,33 +1,30 @@
// Language switching functionality for QR Rapido // 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 () { 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 languageDropdownItems = document.querySelectorAll('.dropdown-item[data-lang]');
const currentLangSpan = document.getElementById('current-lang'); const currentLangSpan = document.getElementById('current-lang');
// Get current culture from URL or default to pt-BR // Get current culture from URL
function getCurrentCulture() { function getCurrentCulture() {
const pathSegments = window.location.pathname.split('/').filter(segment => segment); const pathSegments = window.location.pathname.split('/').filter(segment => segment);
const supportedCultures = ['pt-BR', 'es-PY', 'es'];
if (pathSegments.length > 0 && supportedCultures.includes(pathSegments[0])) { // Check if first segment is es-PY (Spanish)
return pathSegments[0]; if (pathSegments.length > 0 && pathSegments[0].toLowerCase() === 'es-py') {
return 'es-PY';
} }
// Default is Portuguese (no prefix in URL)
return 'pt-BR'; return 'pt-BR';
} }
// Update current language display // Update current language display in header
function updateCurrentLanguageDisplay(culture) { function updateCurrentLanguageDisplay(culture) {
const langMap = { const langMap = {
'pt-BR': 'PT', 'pt-BR': 'PT',
'es-PY': 'ES', 'es-PY': 'ES'
'es': 'ES'
}; };
if (currentLangSpan) { if (currentLangSpan) {
@ -41,40 +38,60 @@ document.addEventListener('DOMContentLoaded', function () {
const queryString = window.location.search; const queryString = window.location.search;
const hash = window.location.hash; const hash = window.location.hash;
// Remove existing culture from path if present // Get path segments, removing any culture prefix
const pathSegments = currentPath.split('/').filter(segment => segment); let pathSegments = currentPath.split('/').filter(segment => segment);
const supportedCultures = ['pt-BR', 'es-PY', 'es'];
// Remove current culture if it's the first segment // Remove existing culture prefix if present (es-PY or pt-BR)
if (pathSegments.length > 0 && supportedCultures.includes(pathSegments[0])) { if (pathSegments.length > 0) {
pathSegments.shift(); const firstSegment = pathSegments[0].toLowerCase();
if (firstSegment === 'es-py' || firstSegment === 'pt-br') {
pathSegments.shift();
}
} }
// Build new path with selected culture // Build new path based on selected culture
const newPath = '/' + newCulture + (pathSegments.length > 0 ? '/' + pathSegments.join('/') : ''); 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; 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 // Handle language selection
languageDropdownItems.forEach(item => { languageDropdownItems.forEach(item => {
item.addEventListener('click', function (e) { item.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
const selectedLang = this.getAttribute('data-lang'); 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 // Track language change for analytics
if (typeof window.trackLanguageChange === 'function') { if (typeof window.trackLanguageChange === 'function') {
window.trackLanguageChange(currentCulture, selectedLang); window.trackLanguageChange(currentCulture, selectedLang);
} }
// Store language preference in localStorage // Save preference
localStorage.setItem('preferredLanguage', selectedLang); setCultureCookie(selectedLang);
// Set culture cookie for server-side processing
document.cookie = `culture=${selectedLang}; path=/; max-age=31536000; SameSite=Lax`;
// Navigate to new URL with selected language // Navigate to new URL with selected language
const newUrl = buildLocalizedUrl(selectedLang); const newUrl = buildLocalizedUrl(selectedLang);
@ -82,46 +99,19 @@ document.addEventListener('DOMContentLoaded', function () {
}); });
}); });
// Initialize current language display // Initialize
currentCulture = getCurrentCulture(); const currentCulture = getCurrentCulture();
updateCurrentLanguageDisplay(currentCulture); updateCurrentLanguageDisplay(currentCulture);
// Store current culture in localStorage if not already set // Ensure cookie matches URL culture
if (!localStorage.getItem('preferredLanguage')) { setCultureCookie(currentCulture);
localStorage.setItem('preferredLanguage', currentCulture);
}
}); });
// Utility function to get user's preferred language // Utility function to get user's preferred language (for external use)
function getUserPreferredLanguage() { function getUserPreferredLanguage() {
// Check localStorage first
const storedLang = localStorage.getItem('preferredLanguage'); const storedLang = localStorage.getItem('preferredLanguage');
if (storedLang) { if (storedLang) {
return storedLang; return storedLang;
} }
return 'pt-BR'; // Default
// 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];
}
} }