From 3135241aaa11389e41478a3ed67a35972d9ec695 Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 21 Mar 2026 20:11:50 -0300 Subject: [PATCH] Feat: Sitemap y Robots --- .gitignore | 1 - Backend/MotoresArgentinosV2.API/Program.cs | 1 + .../Services/SitemapGeneratorService.cs | 127 ++++++++++++++++++ Frontend/nginx.conf | 5 + Frontend/public/robots.txt | 15 +++ docker-compose.yml | 15 ++- 6 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 Backend/MotoresArgentinosV2.Infrastructure/Services/SitemapGeneratorService.cs create mode 100644 Frontend/public/robots.txt diff --git a/.gitignore b/.gitignore index cb7810c..9e4bffc 100644 --- a/.gitignore +++ b/.gitignore @@ -88,7 +88,6 @@ coverage/ #Documentación *.pdf -*.txt #Directorio de Imagenes Backend/MotoresArgentinosV2.API/wwwroot \ No newline at end of file diff --git a/Backend/MotoresArgentinosV2.API/Program.cs b/Backend/MotoresArgentinosV2.API/Program.cs index 86d26c5..4c5c035 100644 --- a/Backend/MotoresArgentinosV2.API/Program.cs +++ b/Backend/MotoresArgentinosV2.API/Program.cs @@ -119,6 +119,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); // 🔒 JWT AUTH var jwtKey = builder.Configuration["Jwt:Key"] ?? throw new InvalidOperationException("JWT Key Missing"); diff --git a/Backend/MotoresArgentinosV2.Infrastructure/Services/SitemapGeneratorService.cs b/Backend/MotoresArgentinosV2.Infrastructure/Services/SitemapGeneratorService.cs new file mode 100644 index 0000000..65dfef4 --- /dev/null +++ b/Backend/MotoresArgentinosV2.Infrastructure/Services/SitemapGeneratorService.cs @@ -0,0 +1,127 @@ +using System.Text; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MotoresArgentinosV2.Core.Entities; +using MotoresArgentinosV2.Infrastructure.Data; + +namespace MotoresArgentinosV2.Infrastructure.Services; + +/// +/// Servicio de fondo que genera sitemap.xml dinámicamente. +/// Ejecuta al iniciar la app y luego cada 6 horas. +/// Incluye rutas estáticas del frontend + vehículos activos. +/// +public class SitemapGeneratorService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private static readonly TimeSpan Interval = TimeSpan.FromHours(6); + + private const string BaseUrl = "https://motoresargentinos.com"; + + // Rutas estáticas públicas del frontend con sus prioridades + private static readonly (string Path, string Priority, string ChangeFreq)[] StaticRoutes = + [ + ("/", "1.0", "daily"), + ("/explorar", "0.8", "daily"), + ("/publicar", "0.6", "monthly"), + ("/vender", "0.6", "monthly"), + ("/condiciones","0.3", "yearly"), + ]; + + public SitemapGeneratorService(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("SitemapGeneratorService iniciado."); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await GenerateSitemapAsync(); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generando sitemap."); + } + + await Task.Delay(Interval, stoppingToken); + } + } + + private async Task GenerateSitemapAsync() + { + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + var config = scope.ServiceProvider.GetRequiredService(); + + // Ruta de salida: configurable para Docker (volumen compartido) o dev local + var outputPath = config["AppSettings:SitemapOutputPath"] ?? Path.Combine("wwwroot", "sitemap.xml"); + + // Obtener todos los avisos activos + var activeAds = await context.Ads + .AsNoTracking() + .Where(a => a.StatusID == (int)AdStatusEnum.Active) + .Select(a => new { a.AdID, a.PublishedAt }) + .ToListAsync(); + + var now = DateTime.UtcNow.ToString("yyyy-MM-dd"); + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine(""); + + // 1. Rutas estáticas + foreach (var (path, priority, changeFreq) in StaticRoutes) + { + sb.AppendLine(" "); + sb.AppendLine($" {BaseUrl}{path}"); + sb.AppendLine($" {now}"); + sb.AppendLine($" {changeFreq}"); + sb.AppendLine($" {priority}"); + sb.AppendLine(" "); + } + + // 2. Rutas dinámicas (vehículos activos) + foreach (var ad in activeAds) + { + var lastmod = ad.PublishedAt?.ToString("yyyy-MM-dd") ?? now; + + sb.AppendLine(" "); + sb.AppendLine($" {BaseUrl}/vehiculo/{ad.AdID}"); + sb.AppendLine($" {lastmod}"); + sb.AppendLine(" weekly"); + sb.AppendLine(" 0.7"); + sb.AppendLine(" "); + } + + sb.AppendLine(""); + + // Escritura atómica: escribir en .tmp, luego mover + var dir = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + + var tempPath = outputPath + ".tmp"; + await File.WriteAllTextAsync(tempPath, sb.ToString(), Encoding.UTF8); + File.Move(tempPath, outputPath, overwrite: true); + + var totalUrls = StaticRoutes.Length + activeAds.Count; + _logger.LogInformation("Sitemap generado con {TotalUrls} URLs ({StaticCount} estáticas + {DynamicCount} vehículos). Archivo: {Path}", + totalUrls, StaticRoutes.Length, activeAds.Count, outputPath); + } +} diff --git a/Frontend/nginx.conf b/Frontend/nginx.conf index 6a90ae7..74e0d43 100644 --- a/Frontend/nginx.conf +++ b/Frontend/nginx.conf @@ -5,6 +5,11 @@ server { # Seguridad: Limitar tamaño de subida para prevenir DoS client_max_body_size 20M; + # Sitemap dinámico (generado por backend en volumen compartido) + location = /sitemap.xml { + alias /usr/share/nginx/html/sitemap-data/sitemap.xml; + } + location / { root /usr/share/nginx/html; index index.html index.htm; diff --git a/Frontend/public/robots.txt b/Frontend/public/robots.txt new file mode 100644 index 0000000..ee581a5 --- /dev/null +++ b/Frontend/public/robots.txt @@ -0,0 +1,15 @@ +User-agent: * +Allow: / +Disallow: /perfil +Disallow: /seguridad +Disallow: /mis-avisos +Disallow: /admin +Disallow: /restablecer-clave +Disallow: /verificar-email +Disallow: /confirmar-cambio-email +Disallow: /baja/ +Disallow: /pago-confirmado +Disallow: /baja-exitosa +Disallow: /baja-error + +Sitemap: https://motoresargentinos.com/sitemap.xml diff --git a/docker-compose.yml b/docker-compose.yml index c57cb76..8cd96b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,38 +5,41 @@ services: dockerfile: Backend/Dockerfile.API container_name: motores-backend restart: always - # Eliminamos ports para que NO sea accesible desde afuera, solo por motores-frontend env_file: - Backend/MotoresArgentinosV2.API/.env environment: - ASPNETCORE_ENVIRONMENT=Production - ASPNETCORE_HTTP_PORTS=8080 - # Soportamos ambos: el dominio final y la IP de pruebas para CORS - - AppSettings__FrontendUrl=https://motoresargentinos.com,http://192.168.5.129:8086,http://localhost:5173,https://clasificados.eldia.com - # Para links generados (pagos/confirmaciones), usamos la IP por ahora si vas a probar sin dominio + - AppSettings__FrontendUrl=https://motoresargentinos.com,https://www.motoresargentinos.com,http://192.168.5.129:8086,http://localhost:5173,https://clasificados.eldia.com - AppSettings__BaseUrl=http://192.168.5.129:8086/api + - AppSettings__SitemapOutputPath=/app/sitemap-output/sitemap.xml networks: - motores-network volumes: - /mnt/MotoresImg:/app/wwwroot/uploads + - sitemap-data:/app/sitemap-output motores-frontend: build: context: ./Frontend dockerfile: Dockerfile args: - # Al usar Nginx como proxy, podemos usar rutas relativas desde el navegador - VITE_API_BASE_URL=/api - VITE_STATIC_BASE_URL= - VITE_MP_PUBLIC_KEY=APP_USR-12bbd874-5ea7-49cf-b9d9-0f3e7df089b3 container_name: motores-frontend restart: always ports: - - "8086:80" # Puerto libre detectado en el análisis de Portainer + - "8086:80" depends_on: - motores-backend networks: - motores-network + volumes: + - sitemap-data:/usr/share/nginx/html/sitemap-data + +volumes: + sitemap-data: networks: motores-network: