OneConversorTemplate/OnlyOneAccessTemplate/Services/SeoService.cs
Ricardo Carneiro b1d75213ab first commit
2025-05-30 23:48:28 -03:00

373 lines
14 KiB
C#

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<SeoService> _logger;
public SeoService(
ISiteConfigurationService siteConfig,
ILanguageService languageService,
IMemoryCache cache,
IConfiguration configuration,
ILogger<SeoService> logger)
{
_siteConfig = siteConfig;
_languageService = languageService;
_cache = cache;
_configuration = configuration;
_logger = logger;
}
public async Task<SeoMetadata> 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<string>("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<string> GenerateStructuredDataAsync(string language, string pageType, object data)
{
try
{
var configuration = await _siteConfig.GetConfigurationAsync(language);
var baseUrl = _configuration.GetValue<string>("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<List<SeoIssue>> ValidateSeoAsync(string url)
{
var issues = new List<SeoIssue>();
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<SeoMetadata> 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<string> GenerateCanonicalUrlAsync(HttpContext context, string language, string pageName)
{
var baseUrl = $"{context.Request.Scheme}://{context.Request.Host}";
return BuildCanonicalUrl(baseUrl, language, pageName);
}
public async Task<Dictionary<string, string>> GenerateMetaTagsAsync(SeoMetadata metadata)
{
return new Dictionary<string, string>
{
{ "title", metadata.Title },
{ "description", metadata.Description },
{ "keywords", metadata.Keywords },
{ "canonical", metadata.CanonicalUrl },
{ "author", metadata.Author ?? "" }
};
}
public async Task<string> GenerateOpenGraphTagsAsync(SeoMetadata metadata)
{
var tags = new StringBuilder();
tags.AppendLine($"<meta property=\"og:title\" content=\"{metadata.OgTitle}\" />");
tags.AppendLine($"<meta property=\"og:description\" content=\"{metadata.OgDescription}\" />");
tags.AppendLine($"<meta property=\"og:image\" content=\"{metadata.OgImage}\" />");
tags.AppendLine($"<meta property=\"og:url\" content=\"{metadata.CanonicalUrl}\" />");
tags.AppendLine($"<meta property=\"og:type\" content=\"{metadata.OgType}\" />");
return tags.ToString();
}
public async Task<string> GenerateTwitterCardTagsAsync(SeoMetadata metadata)
{
var tags = new StringBuilder();
tags.AppendLine($"<meta name=\"twitter:card\" content=\"{metadata.TwitterCard}\" />");
tags.AppendLine($"<meta name=\"twitter:title\" content=\"{metadata.OgTitle}\" />");
tags.AppendLine($"<meta name=\"twitter:description\" content=\"{metadata.OgDescription}\" />");
tags.AppendLine($"<meta name=\"twitter:image\" content=\"{metadata.OgImage}\" />");
return tags.ToString();
}
public async Task<string> GenerateJsonLdAsync(string language, string pageType, object data)
{
return await GenerateStructuredDataAsync(language, pageType, data);
}
public bool ValidateMetadata(SeoMetadata metadata, out List<SeoIssue> issues)
{
issues = new List<SeoIssue>();
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<string, string>(),
StructuredData: "{}"
);
}
}
}