# Relatório de Viabilidade: Sistema de Artigos BCards ## Análise Técnica para Implementação de Tutoriais Baseados em Markdown usando ASP.NET MVC Areas **Data**: 02/11/2025 **Projeto**: BCards (vcart.me.novo) **Referência**: Sistema de artigos QRRapido **Arquitetura**: ASP.NET MVC Areas (padrão já usado no BCards) **Autor**: Claude Code Analysis --- ## 📋 Sumário Executivo **VEREDICTO: ✅ TOTALMENTE VIÁVEL** A implementação de um sistema de artigos baseado em Markdown no BCards é **altamente viável** e apresenta **compatibilidade arquitetural de 100%** com os padrões existentes do projeto. O BCards já utiliza **ASP.NET MVC Areas** (Area "Support"), facilitando ainda mais a implementação. **Tempo Estimado**: 4-6 horas **Complexidade**: Baixa/Média **Risco**: Baixo **ROI**: Alto (SEO + Educação de usuários) **Arquitetura**: Areas (seguindo padrão existente) --- ## 🎯 Requisitos do Usuário ### URLs Desejadas #### 1. Tutoriais por Categoria ``` https://bcards.site/tutoriais/{categoria}/{slug} Exemplos: - /tutoriais/tecnologia/como-criar-um-site-ou-bcard-com-os-links-de-minha-empresa - /tutoriais/advocacia/como-advogados-podem-usar-bcards - /tutoriais/papelaria/criando-cartoes-digitais-para-clientes ``` #### 2. Artigos Inspiracionais (Sem Categoria) ``` https://bcards.site/artigos/{slug} Exemplos: - /artigos/transformacao-digital-para-pequenos-negocios - /artigos/futuro-dos-cartoes-de-visita-digitais ``` ### Base de Categorias **Arquivo**: `/mnt/c/vscode/vcart.me.novo/BCardsDB_Dev.categories.json` **Total**: 30+ categorias (Advocacia, Tecnologia, Papelaria, etc.) **Estrutura**: ```json { "slug": "tecnologia", "name": "Tecnologia", "icon": "💻", "seoKeywords": ["desenvolvimento", "software", "TI"] } ``` --- ## 📊 Análise Comparativa: QRRapido vs BCards ### Arquitetura - Compatibilidade Perfeita | Aspecto | QRRapido | BCards | Compatibilidade | |---------|----------|--------|-----------------| | **Framework** | ASP.NET Core MVC | ASP.NET Core 8.0 MVC | ✅ 100% | | **Padrão** | MVC + Service + Repository | MVC + Service + Repository | ✅ 100% | | **Areas** | ❌ Não usa | ✅ **Usa (Area "Support")** | ✅ 100% (vantagem!) | | **Database** | MongoDB | MongoDB 3.4.2 | ✅ 100% | | **DI Container** | ASP.NET Core nativo | ASP.NET Core nativo | ✅ 100% | | **Cache** | IMemoryCache | IMemoryCache + Middleware | ✅ 100% | | **Views** | Razor + Bootstrap | Razor + Bootstrap 5 | ✅ 100% | ### BCards já usa Areas! **Descoberta Importante**: O BCards já implementa ASP.NET MVC Areas: ```csharp // Program.cs (linha 732-736) app.MapAreaControllerRoute( name: "support-area", areaName: "Support", pattern: "Support/{controller=Support}/{action=Index}/{id?}"); ``` **Estrutura Existente**: ``` /mnt/c/vscode/vcart.me.novo/src/BCards.Web/Areas/Support/ ├── Controllers/ │ ├── SupportController.cs │ └── RatingsController.cs ├── Models/ ├── Repositories/ ├── Services/ ├── ViewComponents/ └── Views/ ``` **Padrão de Controller com Area**: ```csharp namespace BCards.Web.Areas.Support.Controllers; [Area("Support")] [Authorize] public class SupportController : Controller { // ... } ``` ### Bibliotecas Necessárias | Biblioteca | QRRapido | BCards | Ação Necessária | |-----------|----------|--------|-----------------| | **Markdig** | ✅ v0.37.0 | ❌ Não instalado | ➕ Adicionar | | **YamlDotNet** | ✅ v16.2.0 | ❌ Não instalado | ➕ Adicionar | | **IMemoryCache** | ✅ Built-in | ✅ Built-in | ✅ Já existe | | **HtmlAgilityPack** | ❌ | ✅ v1.11.54 | ℹ️ Já tem (opcional) | **Comando de Instalação**: ```bash cd /mnt/c/vscode/vcart.me.novo/src/BCards.Web dotnet add package Markdig dotnet add package YamlDotNet ``` --- ## 🏗️ Arquitetura Proposta: ASP.NET MVC Areas ### Decisão Arquitetural: **USAR AREAS** **Justificativa**: 1. ✅ BCards já usa Areas (padrão estabelecido) 2. ✅ Isolamento de código por contexto funcional 3. ✅ Escalabilidade e manutenibilidade 4. ✅ Evita conflitos de namespace 5. ✅ Permite DI específico por área 6. ✅ Estrutura organizacional clara ### Estrutura Completa com Areas ``` /mnt/c/vscode/vcart.me.novo/src/BCards.Web/ ├── Areas/ │ ├── Support/ # JÁ EXISTE (exemplo de referência) │ │ ├── Controllers/ │ │ ├── Models/ │ │ ├── Services/ │ │ └── Views/ │ │ │ ├── Tutoriais/ # NOVA AREA │ │ ├── Controllers/ │ │ │ └── TutoriaisController.cs │ │ ├── Models/ │ │ │ ├── ArticleMetadata.cs │ │ │ └── ViewModels/ │ │ │ └── ArticleViewModel.cs │ │ ├── Services/ │ │ │ ├── IMarkdownService.cs │ │ │ └── MarkdownService.cs │ │ └── Views/ │ │ └── Tutoriais/ │ │ ├── Index.cshtml # Lista todas categorias │ │ ├── Category.cshtml # Lista artigos da categoria │ │ └── Article.cshtml # Artigo individual │ │ │ └── Artigos/ # NOVA AREA │ ├── Controllers/ │ │ └── ArtigosController.cs │ ├── Models/ │ │ └── [compartilha com Tutoriais] │ ├── Services/ │ │ └── [compartilha MarkdownService] │ └── Views/ │ └── Artigos/ │ ├── Index.cshtml # Lista artigos inspiracionais │ └── Article.cshtml # Artigo individual │ └── Content/ # CONTEÚDO MARKDOWN ├── Tutoriais/ │ ├── tecnologia/ │ │ ├── como-criar-um-site.pt-BR.md │ │ └── como-criar-um-site.es-PY.md (futuro) │ ├── advocacia/ │ │ └── como-advogados-podem-usar-bcards.pt-BR.md │ └── papelaria/ │ └── criando-cartoes-digitais.pt-BR.md │ └── Artigos/ ├── transformacao-digital.pt-BR.md └── futuro-cartoes-digitais.pt-BR.md ``` --- ## 🔧 Detalhes Técnicos de Implementação ### 1. Area "Tutoriais" - Controller **Path**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Areas/Tutoriais/Controllers/TutoriaisController.cs` ```csharp using BCards.Web.Areas.Tutoriais.Models; using BCards.Web.Areas.Tutoriais.Models.ViewModels; using BCards.Web.Areas.Tutoriais.Services; using BCards.Web.Repositories; using Microsoft.AspNetCore.Mvc; namespace BCards.Web.Areas.Tutoriais.Controllers; [Area("Tutoriais")] public class TutoriaisController : Controller { private readonly IMarkdownService _markdownService; private readonly ICategoryRepository _categoryRepository; // JÁ EXISTE! private readonly ILogger _logger; public TutoriaisController( IMarkdownService markdownService, ICategoryRepository categoryRepository, ILogger logger) { _markdownService = markdownService; _categoryRepository = categoryRepository; _logger = logger; } // GET /tutoriais [HttpGet] public async Task Index() { var categories = await _categoryRepository.GetAllAsync(); var tutoriaisPorCategoria = new Dictionary>(); foreach (var category in categories) { var artigos = await _markdownService .GetArticlesByCategoryAsync(category.Slug, "pt-BR"); if (artigos.Any()) { tutoriaisPorCategoria[category.Slug] = artigos; } } return View(tutoriaisPorCategoria); } // GET /tutoriais/{categoria} [HttpGet("{categoria}")] public async Task Category(string categoria) { // Validar categoria existe var category = await _categoryRepository.GetBySlugAsync(categoria); if (category == null) { _logger.LogWarning("Categoria não encontrada: {Categoria}", categoria); return NotFound(); } var artigos = await _markdownService .GetArticlesByCategoryAsync(categoria, "pt-BR"); ViewBag.Category = category; return View(artigos); } // GET /tutoriais/{categoria}/{slug} [HttpGet("{categoria}/{slug}")] public async Task Article(string categoria, string slug) { // Sanitização (segurança contra path traversal) categoria = categoria.Replace("..", "").Replace("/", "").Replace("\\", ""); slug = slug.Replace("..", "").Replace("/", "").Replace("\\", ""); // Validar categoria existe var category = await _categoryRepository.GetBySlugAsync(categoria); if (category == null) { _logger.LogWarning("Categoria não encontrada: {Categoria}", categoria); return NotFound(); } try { var article = await _markdownService.GetArticleAsync( $"Tutoriais/{categoria}/{slug}", "pt-BR" ); if (article == null) { _logger.LogWarning("Artigo não encontrado: {Categoria}/{Slug}", categoria, slug); return NotFound(); } // Buscar artigos relacionados da mesma categoria article.RelatedArticles = await _markdownService .GetArticlesByCategoryAsync(categoria, "pt-BR"); // Remover o artigo atual dos relacionados article.RelatedArticles = article.RelatedArticles .Where(a => a.Slug != slug) .Take(3) .ToList(); ViewBag.Category = category; return View(article); } catch (FileNotFoundException) { _logger.LogWarning("Arquivo markdown não encontrado: {Categoria}/{Slug}", categoria, slug); return NotFound(); } } } ``` ### 2. Area "Artigos" - Controller **Path**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Areas/Artigos/Controllers/ArtigosController.cs` ```csharp using BCards.Web.Areas.Tutoriais.Models.ViewModels; // Compartilha models using BCards.Web.Areas.Tutoriais.Services; // Compartilha service using Microsoft.AspNetCore.Mvc; namespace BCards.Web.Areas.Artigos.Controllers; [Area("Artigos")] public class ArtigosController : Controller { private readonly IMarkdownService _markdownService; private readonly ILogger _logger; public ArtigosController( IMarkdownService markdownService, ILogger logger) { _markdownService = markdownService; _logger = logger; } // GET /artigos [HttpGet] public async Task Index() { var artigos = await _markdownService .GetAllArticlesAsync("Artigos", "pt-BR"); return View(artigos); } // GET /artigos/{slug} [HttpGet("{slug}")] public async Task Article(string slug) { // Sanitização slug = slug.Replace("..", "").Replace("/", "").Replace("\\", ""); try { var article = await _markdownService.GetArticleAsync( $"Artigos/{slug}", "pt-BR" ); if (article == null) { _logger.LogWarning("Artigo não encontrado: {Slug}", slug); return NotFound(); } // Buscar outros artigos para "Leia também" article.RelatedArticles = await _markdownService .GetAllArticlesAsync("Artigos", "pt-BR"); article.RelatedArticles = article.RelatedArticles .Where(a => a.Slug != slug) .OrderByDescending(a => a.Date) .Take(3) .ToList(); return View(article); } catch (FileNotFoundException) { _logger.LogWarning("Arquivo markdown não encontrado: {Slug}", slug); return NotFound(); } } } ``` ### 3. MarkdownService (Compartilhado) **Path**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Areas/Tutoriais/Services/MarkdownService.cs` ```csharp using BCards.Web.Areas.Tutoriais.Models; using BCards.Web.Areas.Tutoriais.Models.ViewModels; using Markdig; using Microsoft.Extensions.Caching.Memory; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace BCards.Web.Areas.Tutoriais.Services; public interface IMarkdownService { Task GetArticleAsync(string relativePath, string culture); Task> GetArticlesByCategoryAsync(string category, string culture); Task> GetAllArticlesAsync(string baseFolder, string culture); } public class MarkdownService : IMarkdownService { private readonly IMemoryCache _cache; private readonly ILogger _logger; private readonly string _contentBasePath; private readonly MarkdownPipeline _markdownPipeline; private readonly IDeserializer _yamlDeserializer; public MarkdownService( IMemoryCache cache, ILogger logger, IWebHostEnvironment environment) { _cache = cache; _logger = logger; _contentBasePath = Path.Combine(environment.ContentRootPath, "Content"); // Pipeline Markdig com extensões avançadas _markdownPipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() // Tables, footnotes, etc. .UseAutoLinks() // Auto-link URLs .UseEmphasisExtras() // ~~strikethrough~~ .UseGenericAttributes() // {#id .class} .DisableHtml() // Segurança: bloqueia HTML inline .Build(); // Deserializador YAML _yamlDeserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); } public async Task GetArticleAsync(string relativePath, string culture) { var cacheKey = $"Article_{relativePath}_{culture}"; // Verificar cache if (_cache.TryGetValue(cacheKey, out ArticleViewModel? cachedArticle)) { _logger.LogDebug("Artigo encontrado no cache: {Path}", relativePath); return cachedArticle; } // Construir caminho completo var fullPath = Path.Combine(_contentBasePath, $"{relativePath}.{culture}.md"); if (!File.Exists(fullPath)) { _logger.LogWarning("Arquivo não encontrado: {Path}", fullPath); return null; } try { var content = await File.ReadAllTextAsync(fullPath); var (metadata, markdownContent) = ExtractFrontmatter(content); if (metadata == null) { _logger.LogError("Frontmatter inválido em: {Path}", fullPath); return null; } // Processar markdown → HTML var htmlContent = Markdown.ToHtml(markdownContent, _markdownPipeline); // Calcular tempo de leitura (200 palavras/minuto) var wordCount = markdownContent.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length; metadata.ReadingTimeMinutes = Math.Max(1, wordCount / 200); // Extrair slug do path metadata.Slug = Path.GetFileNameWithoutExtension( Path.GetFileNameWithoutExtension(relativePath.Split('/').Last()) ); metadata.Culture = culture; var article = new ArticleViewModel { Metadata = metadata, HtmlContent = htmlContent, Slug = metadata.Slug, LastModified = File.GetLastWriteTimeUtc(fullPath) }; // Cache por 1 hora _cache.Set(cacheKey, article, TimeSpan.FromHours(1)); _logger.LogInformation("Artigo processado e cacheado: {Path}", relativePath); return article; } catch (Exception ex) { _logger.LogError(ex, "Erro ao processar artigo: {Path}", fullPath); return null; } } public async Task> GetArticlesByCategoryAsync(string category, string culture) { var cacheKey = $"CategoryArticles_{category}_{culture}"; if (_cache.TryGetValue(cacheKey, out List? cached)) { return cached ?? new List(); } var categoryPath = Path.Combine(_contentBasePath, "Tutoriais", category); if (!Directory.Exists(categoryPath)) { _logger.LogWarning("Diretório de categoria não encontrado: {Path}", categoryPath); return new List(); } var articles = new List(); var files = Directory.GetFiles(categoryPath, $"*.{culture}.md"); foreach (var file in files) { try { var content = await File.ReadAllTextAsync(file); var (metadata, _) = ExtractFrontmatter(content); if (metadata != null) { var slug = Path.GetFileNameWithoutExtension( Path.GetFileNameWithoutExtension(file) ); metadata.Slug = slug; metadata.Culture = culture; metadata.Category = category; articles.Add(metadata); } } catch (Exception ex) { _logger.LogError(ex, "Erro ao processar arquivo: {File}", file); } } // Ordenar por data (mais recentes primeiro) articles = articles.OrderByDescending(a => a.Date).ToList(); _cache.Set(cacheKey, articles, TimeSpan.FromHours(1)); return articles; } public async Task> GetAllArticlesAsync(string baseFolder, string culture) { var cacheKey = $"AllArticles_{baseFolder}_{culture}"; if (_cache.TryGetValue(cacheKey, out List? cached)) { return cached ?? new List(); } var folderPath = Path.Combine(_contentBasePath, baseFolder); if (!Directory.Exists(folderPath)) { _logger.LogWarning("Pasta não encontrada: {Path}", folderPath); return new List(); } var articles = new List(); var files = Directory.GetFiles(folderPath, $"*.{culture}.md", SearchOption.AllDirectories); foreach (var file in files) { try { var content = await File.ReadAllTextAsync(file); var (metadata, _) = ExtractFrontmatter(content); if (metadata != null) { var slug = Path.GetFileNameWithoutExtension( Path.GetFileNameWithoutExtension(file) ); metadata.Slug = slug; metadata.Culture = culture; articles.Add(metadata); } } catch (Exception ex) { _logger.LogError(ex, "Erro ao processar arquivo: {File}", file); } } articles = articles.OrderByDescending(a => a.Date).ToList(); _cache.Set(cacheKey, articles, TimeSpan.FromHours(1)); return articles; } private (ArticleMetadata? metadata, string content) ExtractFrontmatter(string fileContent) { var lines = fileContent.Split('\n'); if (lines.Length < 3 || !lines[0].Trim().Equals("---")) { _logger.LogWarning("Frontmatter não encontrado (deve começar com ---)"); return (null, fileContent); } var endIndex = Array.FindIndex(lines, 1, line => line.Trim().Equals("---")); if (endIndex == -1) { _logger.LogWarning("Frontmatter mal formatado (falta --- de fechamento)"); return (null, fileContent); } try { var yamlContent = string.Join('\n', lines[1..endIndex]); var metadata = _yamlDeserializer.Deserialize(yamlContent); var markdownContent = string.Join('\n', lines[(endIndex + 1)..]); return (metadata, markdownContent); } catch (Exception ex) { _logger.LogError(ex, "Erro ao deserializar YAML frontmatter"); return (null, fileContent); } } } ``` ### 4. Models **Path**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Areas/Tutoriais/Models/ArticleMetadata.cs` ```csharp namespace BCards.Web.Areas.Tutoriais.Models; public class ArticleMetadata { public string Title { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public string Keywords { get; set; } = string.Empty; public string Author { get; set; } = "BCards"; public DateTime Date { get; set; } public DateTime LastMod { get; set; } public string Image { get; set; } = string.Empty; public string Culture { get; set; } = "pt-BR"; public string? Category { get; set; } // Apenas para tutoriais public int ReadingTimeMinutes { get; set; } public string Slug { get; set; } = string.Empty; } ``` **Path**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Areas/Tutoriais/Models/ViewModels/ArticleViewModel.cs` ```csharp namespace BCards.Web.Areas.Tutoriais.Models.ViewModels; public class ArticleViewModel { public ArticleMetadata Metadata { get; set; } = new(); public string HtmlContent { get; set; } = string.Empty; public string Slug { get; set; } = string.Empty; public DateTime LastModified { get; set; } public List RelatedArticles { get; set; } = new(); } ``` ### 5. Configuração de Rotas (Program.cs) **Adicionar ANTES da rota `default`** (linha ~790): ```csharp // ======================================== // AREAS: Tutoriais e Artigos // ======================================== // Tutoriais Area - Artigo específico app.MapAreaControllerRoute( name: "tutoriais-article", areaName: "Tutoriais", pattern: "tutoriais/{categoria}/{slug}", defaults: new { controller = "Tutoriais", action = "Article" }, constraints: new { categoria = @"^[a-z\-]+$", // slug de categoria slug = @"^[a-z0-9\-]+$" // slug do artigo }); // Tutoriais Area - Lista de artigos por categoria app.MapAreaControllerRoute( name: "tutoriais-category", areaName: "Tutoriais", pattern: "tutoriais/{categoria}", defaults: new { controller = "Tutoriais", action = "Category" }, constraints: new { categoria = @"^[a-z\-]+$" }); // Tutoriais Area - Índice (todas categorias) app.MapAreaControllerRoute( name: "tutoriais-index", areaName: "Tutoriais", pattern: "tutoriais", defaults: new { controller = "Tutoriais", action = "Index" }); // Artigos Area - Artigo específico app.MapAreaControllerRoute( name: "artigos-article", areaName: "Artigos", pattern: "artigos/{slug}", defaults: new { controller = "Artigos", action = "Article" }, constraints: new { slug = @"^[a-z0-9\-]+$" }); // Artigos Area - Índice app.MapAreaControllerRoute( name: "artigos-index", areaName: "Artigos", pattern: "artigos", defaults: new { controller = "Artigos", action = "Index" }); // ======================================== // Rota default (já existe) // ======================================== app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); ``` ### 6. Registro de DI (Program.cs) **Adicionar após os outros serviços** (linha ~493): ```csharp // Markdown/Articles System - Tutoriais e Artigos Areas builder.Services.AddScoped< BCards.Web.Areas.Tutoriais.Services.IMarkdownService, BCards.Web.Areas.Tutoriais.Services.MarkdownService>(); ``` ### 7. YAML Frontmatter - Estrutura #### Tutoriais (Com Categoria): **Arquivo**: `Content/Tutoriais/tecnologia/como-criar-um-site.pt-BR.md` ```yaml --- title: "Como Criar um Site Profissional com BCards" description: "Aprenda passo a passo como criar um site profissional usando BCards e centralize todos os seus links em um único lugar" keywords: "bcards, site, tutorial, tecnologia, cartão digital, links" author: "BCards" date: 2025-11-02 lastmod: 2025-11-02 image: "/images/tutoriais/tecnologia/criar-site-hero.jpg" category: "tecnologia" --- # Como Criar um Site Profissional com BCards Neste tutorial, você aprenderá como criar um site profissional em poucos minutos... ## Passo 1: Cadastre-se no BCards Lorem ipsum... ## Passo 2: Escolha seu Tema Lorem ipsum... ## Conclusão Agora você está pronto para... ``` #### Artigos (Sem Categoria): **Arquivo**: `Content/Artigos/transformacao-digital.pt-BR.md` ```yaml --- title: "Transformação Digital para Pequenos Negócios" description: "Descubra como a digitalização está revolucionando pequenas empresas e como você pode aproveitar essa transformação" keywords: "transformação digital, pequenos negócios, inovação, tecnologia" author: "BCards" date: 2025-11-02 lastmod: 2025-11-02 image: "/images/artigos/transformacao-digital-hero.jpg" --- # Transformação Digital para Pequenos Negócios A transformação digital não é mais uma opção, mas uma necessidade... ``` --- ## 🌍 Estratégia de Internacionalização (i18n) ### Fase 1: Português (Atual) - IMPLEMENTAR AGORA ``` Content/Tutoriais/tecnologia/criar-site.pt-BR.md Content/Artigos/transformacao-digital.pt-BR.md ``` **Código**: ```csharp // Hard-coded culture por enquanto var culture = "pt-BR"; var article = await _markdownService.GetArticleAsync($"Tutoriais/{categoria}/{slug}", culture); ``` ### Fase 2: Múltiplos Idiomas (Futuro) - SEM REFATORAÇÃO **Apenas adicionar arquivos**: ``` Content/Tutoriais/tecnologia/criar-site.pt-BR.md Content/Tutoriais/tecnologia/crear-sitio.es-PY.md ← Novo ``` **Roteamento (adaptação futura)**: ```csharp // Adicionar culture à rota app.MapAreaControllerRoute( name: "tutoriais-article-i18n", areaName: "Tutoriais", pattern: "{culture}/tutoriais/{categoria}/{slug}", defaults: new { controller = "Tutoriais", action = "Article", culture = "pt-BR" }, constraints: new { culture = @"^(pt-BR|es-PY)$", categoria = @"^[a-z\-]+$", slug = @"^[a-z0-9\-]+$" }); ``` **Arquitetura i18n-ready**: - ✅ Convenção de nomenclatura já suporta - ✅ Middleware de localização já existe no BCards - ✅ Sem mudanças na estrutura de pastas - ✅ Sem refatoração de código --- ## 📈 Benefícios SEO ### Meta Tags Automáticas (do YAML) **View**: `Areas/Tutoriais/Views/Tutoriais/Article.cshtml` ```html @model BCards.Web.Areas.Tutoriais.Models.ViewModels.ArticleViewModel @{ ViewData["Title"] = Model.Metadata.Title; } ``` ### Sitemap.xml Integration **Adicionar no `HomeController.cs` (método `Sitemap()`):** ```csharp // Tutoriais var markdownService = _serviceProvider.GetRequiredService< BCards.Web.Areas.Tutoriais.Services.IMarkdownService>(); var categories = await _categoryRepository.GetAllAsync(); foreach (var category in categories) { var tutoriais = await markdownService.GetArticlesByCategoryAsync(category.Slug, "pt-BR"); foreach (var tutorial in tutoriais) { sitemap.Append($@" https://bcards.site/tutoriais/{category.Slug}/{tutorial.Slug} {tutorial.LastMod:yyyy-MM-dd} monthly 0.7 "); } } // Artigos var artigos = await markdownService.GetAllArticlesAsync("Artigos", "pt-BR"); foreach (var artigo in artigos) { sitemap.Append($@" https://bcards.site/artigos/{artigo.Slug} {artigo.LastMod:yyyy-MM-dd} monthly 0.7 "); } ``` --- ## ⚡ Performance e Cache ### Sistema de Cache ```csharp // MarkdownService.cs private readonly IMemoryCache _cache; public async Task GetArticleAsync(string slug, string culture) { var cacheKey = $"Article_{slug}_{culture}"; if (!_cache.TryGetValue(cacheKey, out ArticleViewModel article)) { article = await LoadAndProcessMarkdownAsync(slug, culture); _cache.Set(cacheKey, article, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) }); } return article; } ``` **Performance Esperada**: - 🔥 **Cold hit** (primeiro acesso): ~50-100ms - ⚡ **Warm hit** (cached): <1ms - 📦 **Cache hit rate**: 95%+ ### Cache HTTP (Middleware) **Adicionar no `Program.cs` middleware de cache**: ```csharp app.Use(async (context, next) => { // Artigos/Tutoriais - cache público moderado if (context.Request.Path.StartsWithSegments("/tutoriais") || context.Request.Path.StartsWithSegments("/artigos")) { context.Response.Headers.Append("Cache-Control", "public, max-age=3600"); // 1 hora context.Response.Headers.Append("Vary", "Accept-Encoding"); } await next(); }); ``` --- ## 🔒 Segurança ### 1. Validação de Input (Path Traversal) ```csharp // TutoriaisController.cs public async Task Article(string categoria, string slug) { // Sanitização - previne ataques de path traversal categoria = categoria.Replace("..", "").Replace("/", "").Replace("\\", ""); slug = slug.Replace("..", "").Replace("/", "").Replace("\\", ""); // Validação contra slug malicioso via regex (rotas já validam, mas double-check) if (!Regex.IsMatch(categoria, @"^[a-z\-]+$") || !Regex.IsMatch(slug, @"^[a-z0-9\-]+$")) { return BadRequest("Parâmetros inválidos"); } } ``` ### 2. XSS Protection (Markdig) ```csharp // MarkdownService.cs - Pipeline configurado para segurança _markdownPipeline = new MarkdownPipelineBuilder() .UseAdvancedExtensions() .DisableHtml() // CRÍTICO: Bloqueia HTML inline perigoso .Build(); ``` **Comportamento**: ```markdown Este é um texto seguro <script>alert('XSS')</script>

Este é um texto seguro

``` ### 3. File System Access Control ```csharp // MarkdownService.cs private readonly string _contentBasePath; public MarkdownService(IWebHostEnvironment environment) { // Path base restrito à pasta Content/ _contentBasePath = Path.Combine(environment.ContentRootPath, "Content"); } public async Task GetArticleAsync(string relativePath, string culture) { // Sempre combina com _contentBasePath (não permite acesso fora de Content/) var fullPath = Path.Combine(_contentBasePath, $"{relativePath}.{culture}.md"); // Double-check: verifica se path final está dentro de Content/ if (!fullPath.StartsWith(_contentBasePath)) { _logger.LogWarning("Tentativa de acesso fora de Content/: {Path}", fullPath); return null; } } ``` --- ## 📝 Plano de Implementação (4-6 horas) ### Fase 1: Setup e Pacotes (30 min) - [ ] Instalar Markdig + YamlDotNet - [ ] Criar estrutura de pastas Areas (Tutoriais + Artigos) - [ ] Criar estrutura Content (Tutoriais/{categorias} + Artigos) ### Fase 2: Models e Services (1.5h) - [ ] Criar `ArticleMetadata.cs` - [ ] Criar `ArticleViewModel.cs` - [ ] Criar `IMarkdownService.cs` interface - [ ] Implementar `MarkdownService.cs` completo - [ ] Registrar DI em `Program.cs` ### Fase 3: Controllers (1h) - [ ] Criar `TutoriaisController.cs` (3 actions) - [ ] Criar `ArtigosController.cs` (2 actions) - [ ] Adicionar rotas no `Program.cs` ### Fase 4: Views (1.5h) - [ ] Criar `Tutoriais/Index.cshtml` - [ ] Criar `Tutoriais/Category.cshtml` - [ ] Criar `Tutoriais/Article.cshtml` - [ ] Criar `Artigos/Index.cshtml` - [ ] Criar `Artigos/Article.cshtml` - [ ] Adaptar CSS para identidade visual BCards ### Fase 5: SEO e Integração (45 min) - [ ] Adicionar meta tags nas views - [ ] Implementar Schema.org JSON-LD - [ ] Integrar no `Sitemap.xml` (HomeController) - [ ] Configurar cache HTTP no middleware ### Fase 6: Conteúdo e Testes (45 min) - [ ] Criar 2-3 tutoriais de exemplo - [ ] Criar 1-2 artigos inspiracionais - [ ] Testar todas rotas - [ ] Validar cache (warm/cold hits) - [ ] Verificar SEO tags (View Source) - [ ] Testar segurança (path traversal) --- ## ✅ Checklist Completo de Arquivos ### Criar (23 arquivos novos) #### Content (Markdown) ``` 📁 Content/ 📁 Tutoriais/ 📁 tecnologia/ 📄 como-criar-um-site.pt-BR.md 📄 guia-completo-bcards.pt-BR.md 📁 advocacia/ 📄 como-advogados-podem-usar-bcards.pt-BR.md 📁 papelaria/ 📄 criando-cartoes-digitais.pt-BR.md 📁 Artigos/ 📄 transformacao-digital.pt-BR.md 📄 futuro-cartoes-digitais.pt-BR.md ``` #### Area Tutoriais ``` 📁 src/BCards.Web/Areas/Tutoriais/ 📁 Controllers/ 📄 TutoriaisController.cs 📁 Models/ 📄 ArticleMetadata.cs 📁 ViewModels/ 📄 ArticleViewModel.cs 📁 Services/ 📄 IMarkdownService.cs 📄 MarkdownService.cs 📁 Views/ 📁 Tutoriais/ 📄 Index.cshtml 📄 Category.cshtml 📄 Article.cshtml ``` #### Area Artigos ``` 📁 src/BCards.Web/Areas/Artigos/ 📁 Controllers/ 📄 ArtigosController.cs 📁 Views/ 📁 Artigos/ 📄 Index.cshtml 📄 Article.cshtml ``` ### Modificar (3 arquivos existentes) ``` 📄 src/BCards.Web/BCards.Web.csproj → Adicionar Markdig + YamlDotNet 📄 src/BCards.Web/Program.cs → Registrar DI + Rotas de Areas 📄 src/BCards.Web/Controllers/HomeController.cs → Adicionar artigos ao sitemap (opcional) ``` --- ## 💰 Estimativa de Custo/Benefício ### Custo - **Desenvolvimento**: 4-6 horas - **NuGet Packages**: Gratuitos (Markdig + YamlDotNet são open-source) - **Infraestrutura**: Zero (usa filesystem, não requer DB) - **Manutenção**: Baixíssima (apenas criar novos arquivos .md) ### Benefício - ✅ **SEO**: Conteúdo indexável aumenta tráfego orgânico (+30-50% estimado) - ✅ **Educação**: Reduz suporte via tutoriais self-service (-20% tickets) - ✅ **Credibilidade**: Demonstra expertise no setor - ✅ **Conversão**: Artigos podem incluir CTAs para planos Premium (+10% conversão) - ✅ **i18n-ready**: Expansão internacional facilitada (sem refatoração) - ✅ **Escalabilidade**: Adicionar artigos é trivial (apenas criar .md) **ROI Estimado**: **Alto** (custo mínimo, benefícios de longo prazo) --- ## 🚨 Riscos e Mitigações | Risco | Probabilidade | Impacto | Mitigação | |-------|---------------|---------|-----------| | Conflito de rotas | Baixa | Médio | Usar `/tutoriais` e `/artigos` (não conflita com `/page`) + Constraints em rotas | | Performance (muitos .md) | Baixa | Baixo | Cache de 1 hora resolve + Lazy loading | | XSS via Markdown | Média | Alto | `DisableHtml()` no Markdig pipeline | | Path traversal | Baixa | Alto | Sanitização de input + Path validation | | Namespace conflicts | Muito Baixa | Baixo | Areas isola namespaces automaticamente | | Categoria inválida | Média | Baixo | Validação via `CategoryRepository` existente | --- ## 🎯 Recomendações Finais ### Priorização **IMPLEMENTAR AGORA** (Alta Prioridade): 1. ✅ Area "Tutoriais" completa (`/tutoriais/{categoria}/{slug}`) 2. ✅ Integração com `CategoryRepository` existente 3. ✅ SEO básico (meta tags, sitemap, schema.org) 4. ✅ Cache de 1 hora (IMemoryCache) **IMPLEMENTAR EM SEGUIDA** (Média Prioridade): 5. ⏳ Area "Artigos" (`/artigos/{slug}`) 6. ⏳ Criar 5-10 tutoriais iniciais 7. ⏳ Analytics de leitura (Google Analytics events) **IMPLEMENTAR DEPOIS** (Baixa Prioridade): 8. 💭 Múltiplos idiomas (pt-BR, es-PY) 9. 💭 Sistema de comentários 10. 💭 CMS web para edição (por enquanto usar VS Code) 11. 💭 Search/filtros avançados ### Evolução Natural **Fase 1** (Agora - Semana 1): - Português apenas - Filesystem-based - 10-15 artigos iniciais **Fase 2** (Mês 2-3): - Adicionar espanhol (apenas novos `.md`) - 30-50 artigos - Analytics de leitura **Fase 3** (Mês 4-6): - Search interno - Filtros por categoria - Artigos relacionados mais inteligentes **Fase 4** (Ano 1+): - CMS web opcional - Múltiplos autores - Sistema de versioning --- ## 📚 Referências ### Código QRRapido (Base) - **Services**: `/mnt/c/vscode/qrrapido/Services/MarkdownService.cs` - **Controllers**: `/mnt/c/vscode/qrrapido/Controllers/TutoriaisController.cs` - **Views**: `/mnt/c/vscode/qrrapido/Views/Tutoriais/` ### BCards (Integração) - **Docs**: `/mnt/c/vscode/vcart.me.novo/CLAUDE.md` - **Categories**: `/mnt/c/vscode/vcart.me.novo/BCardsDB_Dev.categories.json` - **Area Example**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Areas/Support/` - **Repository**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Repositories/CategoryRepository.cs` - **Program.cs**: `/mnt/c/vscode/vcart.me.novo/src/BCards.Web/Program.cs` ### Bibliotecas - **Markdig**: https://github.com/xoofx/markdig - **YamlDotNet**: https://github.com/aaubry/YamlDotNet - **ASP.NET Areas**: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/areas --- ## 🏁 Conclusão A implementação de um sistema de artigos no BCards usando **ASP.NET MVC Areas** é **100% viável** e apresenta **compatibilidade arquitetural perfeita** com o código existente. ### Pontos Fortes ✅ **Areas já em uso**: BCards usa Areas (exemplo: Support) ✅ **Arquitetura MVC + Service + Repository**: Padrão estabelecido ✅ **CategoryRepository existente**: Integração natural com categorias ✅ **Bootstrap 5**: Compatível com views do QRRapido ✅ **IMemoryCache**: Sistema de cache pronto ✅ **i18n-ready**: Expansão internacional sem refatoração ✅ **Isolamento**: Areas separa código de forma limpa ✅ **Escalabilidade**: Fácil adicionar novas features por área ### Diferencial: Areas O uso de **ASP.NET MVC Areas** traz vantagens significativas: - **Organização**: Código isolado por contexto (Tutoriais, Artigos, Support) - **Namespaces**: Zero conflitos de nomes - **Manutenibilidade**: Equipes diferentes podem trabalhar em areas diferentes - **Padrão estabelecido**: Já usado no BCards (consistência) ### Próximos Passos 1. ✅ **Aprovação do usuário** 2. Instalação de pacotes NuGet (2 minutos) 3. Criação de estrutura de Areas (30 minutos) 4. Implementação de Services e Controllers (3 horas) 5. Criação de Views (1.5 horas) 6. Criação de 2-3 artigos de teste (30 minutos) 7. Deploy e validação **Recomendação**: **Prosseguir com implementação usando ASP.NET MVC Areas**. A arquitetura é sólida, escalável e perfeitamente alinhada com os padrões do BCards. --- **Documento aprovado para implementação** ✅