using Microsoft.Extensions.Caching.Memory; using System.Text; using System.Text.Json; using System.Linq; using OnlyOneAccessTemplate.Models; using global::OnlyOneAccessTemplate.Models; namespace OnlyOneAccessTemplate.Services { public class SeoService : ISeoService { private readonly ISiteConfigurationService _siteConfig; private readonly ILanguageService _languageService; private readonly IMemoryCache _cache; private readonly IConfiguration _configuration; private readonly ILogger _logger; public SeoService( ISiteConfigurationService siteConfig, ILanguageService languageService, IMemoryCache cache, IConfiguration configuration, ILogger logger) { _siteConfig = siteConfig; _languageService = languageService; _cache = cache; _configuration = configuration; _logger = logger; } public async Task GenerateSeoMetadataAsync(string language, string pageName, PageContent? content = null) { var cacheKey = $"seo_metadata_{language}_{pageName}"; if (_cache.TryGetValue(cacheKey, out SeoMetadata? cachedMetadata) && cachedMetadata != null) { return cachedMetadata; } try { var configuration = await _siteConfig.GetConfigurationAsync(language); content = content ?? await _siteConfig.GetPageContentAsync(language, pageName); var baseUrl = _configuration.GetValue("SEO:DefaultDomain") ?? "https://localhost"; var canonicalUrl = BuildCanonicalUrl(baseUrl, language, pageName); var hreflangUrls = _languageService.GetHreflangUrls(canonicalUrl, language); var metadata = new SeoMetadata( Title: content?.MetaTitle ?? content?.Title ?? configuration.Seo.DefaultTitle, Description: content?.Description ?? configuration.Seo.DefaultDescription, Keywords: content?.Keywords ?? configuration.Seo.DefaultKeywords, OgTitle: content?.Title ?? configuration.Seo.DefaultTitle, OgDescription: content?.Description ?? configuration.Seo.DefaultDescription, OgImage: content?.OgImage ?? configuration.Seo.OgImage, CanonicalUrl: canonicalUrl, HreflangUrls: hreflangUrls, StructuredData: await GenerateStructuredDataAsync(language, "WebPage", new { content, configuration }) ); _cache.Set(cacheKey, metadata, TimeSpan.FromMinutes(15)); return metadata; } catch (Exception ex) { _logger.LogError(ex, "Erro ao gerar metadata SEO para {Language}/{PageName}", language, pageName); return CreateFallbackMetadata(language, pageName); } } public async Task GenerateStructuredDataAsync(string language, string pageType, object data) { try { var configuration = await _siteConfig.GetConfigurationAsync(language); var baseUrl = _configuration.GetValue("SEO:DefaultDomain") ?? "https://localhost"; var structuredData = pageType.ToLowerInvariant() switch { "webpage" => GenerateWebPageStructuredData(configuration, baseUrl, language, data), "organization" => GenerateOrganizationStructuredData(configuration, baseUrl), "breadcrumb" => GenerateBreadcrumbStructuredData(baseUrl, language, data), "faq" => GenerateFaqStructuredData(data), "product" => GenerateProductStructuredData(configuration, data), _ => GenerateWebPageStructuredData(configuration, baseUrl, language, data) }; return JsonSerializer.Serialize(structuredData, new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); } catch (Exception ex) { _logger.LogError(ex, "Erro ao gerar structured data para {PageType}", pageType); return "{}"; } } public async Task> ValidateSeoAsync(string url) { var issues = new List(); try { // Esta implementação seria expandida com validações reais // Por enquanto, retorna uma lista básica de verificações if (string.IsNullOrEmpty(url)) { issues.Add(new SeoIssue("URL", "URL não fornecida", "High", "")); } // Adicionar mais validações conforme necessário // - Verificar se title tem tamanho adequado // - Verificar se description tem tamanho adequado // - Verificar se há headings estruturados // - Verificar se há alt text nas imagens // - Verificar velocidade da página // - Verificar responsividade return issues; } catch (Exception ex) { _logger.LogError(ex, "Erro ao validar SEO para URL {Url}", url); issues.Add(new SeoIssue("Validation", "Erro na validação SEO", "High", "")); return issues; } } private object GenerateWebPageStructuredData(SiteConfiguration config, string baseUrl, string language, object data) { return new { Context = "https://schema.org", Type = "WebPage", Name = config.Seo.SiteName, Description = config.Seo.DefaultDescription, Url = baseUrl, InLanguage = language, IsPartOf = new { Type = "WebSite", Name = config.Seo.SiteName, Url = baseUrl }, DatePublished = DateTime.UtcNow.ToString("yyyy-MM-dd"), DateModified = DateTime.UtcNow.ToString("yyyy-MM-dd") }; } public async Task GenerateMetadataForPageAsync(HttpContext context, string language, string pageName, PageContent? customContent = null) { var currentUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}"; return await GenerateSeoMetadataAsync(language, pageName, customContent); } public async Task GenerateCanonicalUrlAsync(HttpContext context, string language, string pageName) { var baseUrl = $"{context.Request.Scheme}://{context.Request.Host}"; return BuildCanonicalUrl(baseUrl, language, pageName); } public async Task> GenerateMetaTagsAsync(SeoMetadata metadata) { return new Dictionary { { "title", metadata.Title }, { "description", metadata.Description }, { "keywords", metadata.Keywords }, { "canonical", metadata.CanonicalUrl }, { "author", metadata.Author ?? "" } }; } public async Task GenerateOpenGraphTagsAsync(SeoMetadata metadata) { var tags = new StringBuilder(); tags.AppendLine($""); tags.AppendLine($""); tags.AppendLine($""); tags.AppendLine($""); tags.AppendLine($""); return tags.ToString(); } public async Task GenerateTwitterCardTagsAsync(SeoMetadata metadata) { var tags = new StringBuilder(); tags.AppendLine($""); tags.AppendLine($""); tags.AppendLine($""); tags.AppendLine($""); return tags.ToString(); } public async Task GenerateJsonLdAsync(string language, string pageType, object data) { return await GenerateStructuredDataAsync(language, pageType, data); } public bool ValidateMetadata(SeoMetadata metadata, out List issues) { issues = new List(); if (string.IsNullOrEmpty(metadata.Title)) { issues.Add(new SeoIssue("Title", "Título não pode estar vazio", "High", "title")); } else if (metadata.Title.Length > 60) { issues.Add(new SeoIssue("Title", "Título muito longo (máx. 60 caracteres)", "Medium", "title")); } if (string.IsNullOrEmpty(metadata.Description)) { issues.Add(new SeoIssue("Description", "Descrição não pode estar vazia", "High", "meta[name='description']")); } else if (metadata.Description.Length > 160) { issues.Add(new SeoIssue("Description", "Descrição muito longa (máx. 160 caracteres)", "Medium", "meta[name='description']")); } if (string.IsNullOrEmpty(metadata.OgImage)) { issues.Add(new SeoIssue("OpenGraph", "Imagem Open Graph não definida", "Medium", "meta[property='og:image']")); } return !issues.Any(i => i.Severity == "High"); } private object GenerateOrganizationStructuredData(SiteConfiguration config, string baseUrl) { return new { Context = "https://schema.org", Type = "Organization", Name = config.Seo.SiteName, Url = baseUrl, Logo = new { Type = "ImageObject", Url = $"{baseUrl}/logo.png" }, SameAs = new[] { "Facebook" } }; } private object GenerateBreadcrumbStructuredData(string baseUrl, string language, object data) { return new { Context = "https://schema.org", Type = "BreadcrumbList", ItemListElement = new[] { new { Type = "ListItem", Position = 1, Name = language switch { "en" => "Home", "es" => "Inicio", _ => "Início" }, Item = baseUrl } // Adicionar mais items do breadcrumb conforme necessário } }; } private object GenerateFaqStructuredData(object data) { return new { Context = "https://schema.org", Type = "FAQPage", MainEntity = new[] { // Exemplo de FAQ - expandir conforme necessário new { Type = "Question", Name = "Como funciona?", AcceptedAnswer = new { Type = "Answer", Text = "Explicação de como funciona..." } } } }; } private object GenerateProductStructuredData(SiteConfiguration config, object data) { return new { Context = "https://schema.org", Type = "Product", Name = "Nome do Produto", Description = "Descrição do produto", Brand = new { Type = "Brand", Name = config.Seo.SiteName }, Offers = new { Type = "Offer", PriceCurrency = "BRL", Price = "0", Availability = "https://schema.org/InStock" } }; } private string BuildCanonicalUrl(string baseUrl, string language, string pageName) { var url = baseUrl.TrimEnd('/'); if (language != _languageService.GetDefaultLanguage()) { url += $"/{language}"; } if (!string.IsNullOrEmpty(pageName) && pageName != "index") { url += $"/{pageName}"; } return url; } private SeoMetadata CreateFallbackMetadata(string language, string pageName) { var defaultTitle = language switch { "en" => "Page Not Found", "es" => "Página No Encontrada", _ => "Página Não Encontrada" }; var defaultDescription = language switch { "en" => "The requested page could not be found.", "es" => "La página solicitada no pudo ser encontrada.", _ => "A página solicitada não pôde ser encontrada." }; return new SeoMetadata( Title: defaultTitle, Description: defaultDescription, Keywords: "", OgTitle: defaultTitle, OgDescription: defaultDescription, OgImage: "", CanonicalUrl: "", HreflangUrls: new Dictionary(), StructuredData: "{}" ); } } }