Compare commits

..

No commits in common. "main" and "Release/ajuda-console" have entirely different histories.

40 changed files with 1838 additions and 3253 deletions

View File

@ -35,27 +35,7 @@
"Bash(ss:*)",
"Bash(lsof:*)",
"Bash(dotnet run:*)",
"Bash(dotnet user-secrets:*)",
"Bash(xargs grep:*)",
"mcp__stripe__list_products",
"mcp__stripe__list_prices",
"mcp__stripe__get_stripe_account_info",
"mcp__stripe__search_stripe_resources",
"Bash(docker exec:*)",
"mcp__stripe__list_subscriptions",
"mcp__stripe__create_product",
"mcp__stripe__create_price",
"Bash(git push:*)",
"mcp__stripe__stripe_api_execute",
"mcp__stripe__stripe_api_search",
"Bash(ctx csharp:*)",
"Bash(ctx auto:*)",
"Bash(git log:*)",
"Bash(git mv:*)",
"Bash(python3 -c \"import sys,json; [print\\(json.loads\\(l\\).get\\('MessageTemplate',''\\) + ' ' + str\\(json.loads\\(l\\).get\\('Level',''\\)\\)\\) for l in sys.stdin if 'categ' in l.lower\\(\\) or 'Error' in l or 'error' in l.lower\\(\\)]\")",
"Bash(python3 -c \" import sys, json for line in sys.stdin: line = line.strip\\(\\).rstrip\\(','\\) try: obj = json.loads\\(line\\) lvl = obj.get\\('Level',''\\) msg = obj.get\\('MessageTemplate',''\\) or obj.get\\('message',''\\) if lvl in \\('Error','Warning','Fatal'\\) or 'categ' in msg.lower\\(\\) or 'initial' in msg.lower\\(\\): print\\(f'[{lvl}] {msg}'\\) except: pass \")",
"Bash(grep -E \"\\\\.\\(cs|csproj\\)$\")",
"Bash(git -C /c/vscode/bcards log --oneline --follow src/BCards.Web/Views/Admin/CreatePage.cshtml)"
"Bash(dotnet user-secrets:*)"
]
},
"enableAllProjectMcpServers": false

View File

@ -21,8 +21,8 @@ name: BCards Multi-Tenant Deployment Pipeline
# ─── Per-Tenant Variables (optional, have defaults) ───────────────────────
# vars.SPICYLINKS_FROM_EMAIL (default: noreply@spicylinks.site)
# vars.SPICYLINKS_FROM_NAME (default: SpicyLinks)
# vars.LUZLINKS_FROM_EMAIL (default: noreply@luzlinks.site)
# vars.LUZLINKS_FROM_NAME (default: LuzLinks)
# vars.LUSLINKS_FROM_EMAIL (default: noreply@luslinks.site)
# vars.LUSLINKS_FROM_NAME (default: LusLinks)
on:
push:
@ -212,14 +212,14 @@ jobs:
"FromName": "${{ vars.SENDGRID_FROM_NAME || 'Ricardo Carneiro' }}"
},
"Plans": {
"Basic": { "Name": "Básico", "PriceId": "price_1TR10MBk8jHwC3c0iey23Ghb", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" },
"Professional": { "Name": "Profissional", "PriceId": "price_1TR10NBk8jHwC3c0yqmy8soD", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" },
"Premium": { "Name": "Premium", "PriceId": "price_1TR10OBk8jHwC3c0eZa77y31", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)" ], "Interval": "month" },
"PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1TR10PBk8jHwC3c0B1oIvvYY", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)" ], "Interval": "month" },
"BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1TR10NBk8jHwC3c0L4SDaWe9", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], "Interval": "year" },
"ProfessionalYearly": { "Name": "Profissional Anual", "PriceId": "price_1TR10OBk8jHwC3c0IuyvrvRf", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumYearly": { "Name": "Premium Anual", "PriceId": "price_1TR10PBk8jHwC3c0qngPYMUN", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1TR10QBk8jHwC3c0f8CBaD1n", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)", "Economize R$ 59,80 (2 meses grátis)" ], "Interval": "year" }
"Basic": { "Name": "Básico", "PriceId": "price_1RycPaBMIadsOxJVKioZZofK", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" },
"Professional": { "Name": "Profissional", "PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" },
"Premium": { "Name": "Premium", "PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)" ], "Interval": "month" },
"PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1RycTaBMIadsOxJVeDLseXQq", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)" ], "Interval": "month" },
"BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], "Interval": "year" },
"ProfessionalYearly": { "Name": "Profissional Anual", "PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumYearly": { "Name": "Premium Anual", "PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados", "Upload de PDFs (até 5 arquivos)", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Upload de PDFs (até 10 arquivos)", "Economize R$ 59,80 (2 meses grátis)" ], "Interval": "year" }
},
"Moderation": {
"PriorityTimeframes": { "Trial": "7.00:00:00", "Basic": "7.00:00:00", "Professional": "3.00:00:00", "Premium": "1.00:00:00" },
@ -254,10 +254,7 @@ jobs:
"CtaDescription": "Junte-se a milhares de profissionais que já têm sua presença digital organizada no BCards.",
"CtaButtonText": "Criar Minha Página Grátis",
"MetaKeywords": "cartão digital, página de links, bio links, linktree brasil, página profissional, corretor, advogado, médico, consultor",
"FooterTagline": "Sua presença digital profissional, simplificada.",
"HeroGradient": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"PrimaryColor": "#667eea",
"PrimaryColorDark": "#5a6fd6"
"FooterTagline": "Sua presença digital profissional, simplificada."
},
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
@ -285,7 +282,7 @@ jobs:
for NODE in ${{ env.SWARM_MANAGER }} ${{ env.SWARM_WORKER }}; do
echo "📂 Syncing content to $NODE..."
ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/bcards && sudo chown -R ubuntu:ubuntu /opt/bcards-content'
rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" src/BCards.Web/Content/Tenants/bcards/ ubuntu@$NODE:/opt/bcards-content/bcards/
scp -o StrictHostKeyChecking=no -r src/BCards.Web/Content/Tenants/bcards/. ubuntu@$NODE:/opt/bcards-content/bcards/
done
# ── Deploy stack on manager ─────────────────────────────────────────
@ -360,14 +357,14 @@ jobs:
"FromName": "${{ vars.SPICYLINKS_FROM_NAME || 'SpicyLinks' }}"
},
"Plans": {
"Basic": { "Name": "Básico", "PriceId": "price_1TR10QBk8jHwC3c06u8j4XVY", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" },
"Professional": { "Name": "Profissional", "PriceId": "price_1TR10RBk8jHwC3c0KfunnYYn", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" },
"Premium": { "Name": "Premium", "PriceId": "price_1TR10SBk8jHwC3c0gMqUEp7m", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ], "Interval": "month" },
"PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1TR10TBk8jHwC3c0uPOZVZ4P", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], "Interval": "month" },
"BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1TR10RBk8jHwC3c0C8aOMAYE", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], "Interval": "year" },
"ProfessionalYearly": { "Name": "Profissional Anual", "PriceId": "price_1TR10SBk8jHwC3c0X7LBy3UU", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumYearly": { "Name": "Premium Anual", "PriceId": "price_1TR10TBk8jHwC3c0TaDIA6bD", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1TR10UBk8jHwC3c0NF66MzC7", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Economize R$ 59,80 (2 meses grátis)" ], "Interval": "year" }
"Basic": { "Name": "Básico", "PriceId": "price_1RycPaBMIadsOxJVKioZZofK", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" },
"Professional": { "Name": "Profissional", "PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" },
"Premium": { "Name": "Premium", "PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas (básicos + premium)", "Suporte prioritário", "Links ilimitados" ], "Interval": "month" },
"PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1RycTaBMIadsOxJVeDLseXQq", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], "Interval": "month" },
"BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], "Interval": "year" },
"ProfessionalYearly": { "Name": "Profissional Anual", "PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumYearly": { "Name": "Premium Anual", "PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Economize R$ 59,80 (2 meses grátis)" ], "Interval": "year" }
},
"Moderation": {
"PriorityTimeframes": { "Trial": "7.00:00:00", "Basic": "7.00:00:00", "Professional": "3.00:00:00", "Premium": "1.00:00:00" },
@ -402,30 +399,7 @@ jobs:
"CtaDescription": "Milhares de criadoras já centralizam seus links e aumentam suas conversões com o SpicyLinks.",
"CtaButtonText": "Criar Minha Bio",
"MetaKeywords": "bio links criadora, creator bio, linktree conteudo adulto, links onlyfans, bio instagram criadora",
"FooterTagline": "Seu conteúdo, sua identidade.",
"HeroGradient": "linear-gradient(135deg, #ff416c 0%, #c0392b 100%)",
"PrimaryColor": "#e63946",
"PrimaryColorDark": "#c1121f",
"DefaultCategories": [
{ "Icon": "📸", "Name": "Modelos", "Slug": "modelos", "Description": "Modelos e criadores de conteúdo visual", "SeoKeywords": [ "modelo", "fotografia", "conteúdo", "criadora" ] },
{ "Icon": "⭐", "Name": "Influencers", "Slug": "influencers","Description": "Influencers e personalidades digitais", "SeoKeywords": [ "influencer", "digital", "social media" ] },
{ "Icon": "💪", "Name": "Fitness", "Slug": "fitness", "Description": "Criadores de conteúdo fitness e lifestyle", "SeoKeywords": [ "fitness", "academia", "saúde", "corpo" ] },
{ "Icon": "🎨", "Name": "Arte", "Slug": "arte", "Description": "Artistas e criadores de conteúdo visual", "SeoKeywords": [ "arte", "ilustração", "design", "criativo" ] },
{ "Icon": "🎵", "Name": "Música", "Slug": "musica", "Description": "Músicos e cantores independentes", "SeoKeywords": [ "música", "cantor", "artista", "show" ] },
{ "Icon": "🎮", "Name": "Gaming", "Slug": "gaming", "Description": "Streamers e criadores de conteúdo gamer", "SeoKeywords": [ "gaming", "streamer", "games", "twitch" ] },
{ "Icon": "🦸", "Name": "Cosplay", "Slug": "cosplay", "Description": "Cosplayers e criadores de fantasia", "SeoKeywords": [ "cosplay", "anime", "fantasia", "cosplayer" ] },
{ "Icon": "💋", "Name": "Lifestyle", "Slug": "lifestyle", "Description": "Criadores de conteúdo lifestyle e entretenimento", "SeoKeywords": [ "lifestyle", "entretenimento", "diversão" ] }
],
"AllowedLinkTypes": [
{ "Icon": "fas fa-globe", "Label": "🌐 Site Geral", "Prefix": "https://", "Placeholder": "exemplo.com", "Instructions": "Digite o domínio e caminho", "Color": "bg-primary" },
{ "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "seuemail@exemplo.com", "Instructions": "Digite apenas o email", "Color": "bg-success" },
{ "Icon": "fas fa-phone", "Label": "📞 Telefone", "Prefix": "tel:", "Placeholder": "5511999999999", "Instructions": "Número com código do país", "Color": "bg-success" },
{ "Icon": "fab fa-instagram", "Label": "📸 Instagram", "Prefix": "https://instagram.com/","Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-danger" },
{ "Icon": "fab fa-twitter", "Label": "🐦 Twitter/X", "Prefix": "https://x.com/", "Placeholder": "seu_usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-dark" },
{ "Icon": "fab fa-tiktok", "Label": "🎵 TikTok", "Prefix": "https://tiktok.com/@", "Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-dark" },
{ "Icon": "fas fa-shopping-cart","Label": "🛒 Lista de Desejos","Prefix": "https://", "Placeholder": "wishlist.com/...", "Instructions": "Link para lista de desejos", "Color": "bg-warning" },
{ "Icon": "fas fa-heart", "Label": "❤️ Assinatura", "Prefix": "https://", "Placeholder": "plataforma.com/...", "Instructions": "Link para plataforma paga", "Color": "bg-danger" }
]
"FooterTagline": "Seu conteúdo, sua identidade."
},
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
@ -453,7 +427,7 @@ jobs:
for NODE in ${{ env.SWARM_MANAGER }} ${{ env.SWARM_WORKER }}; do
echo "📂 Syncing spicylinks content to $NODE..."
ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/spicylinks && sudo chown -R ubuntu:ubuntu /opt/bcards-content'
rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" src/BCards.Web/Content/Tenants/spicylinks/ ubuntu@$NODE:/opt/bcards-content/spicylinks/
scp -o StrictHostKeyChecking=no -r src/BCards.Web/Content/Tenants/spicylinks/. ubuntu@$NODE:/opt/bcards-content/spicylinks/
done
# ── Deploy stack on manager ─────────────────────────────────────────
@ -482,9 +456,9 @@ jobs:
ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} \
'curl -sf http://localhost:8082/health && echo "✅ spicylinks healthy" || echo "⚠️ spicylinks health check failed"'
# ─── Deploy: luzlinks.site ────────────────────────────────────────────────
deploy-luzlinks:
name: Deploy luzlinks.site
# ─── Deploy: luslinks.site ────────────────────────────────────────────────
deploy-luslinks:
name: Deploy luslinks.site
runs-on: ubuntu-latest
needs: [build-and-push]
if: github.ref_name == 'main'
@ -494,9 +468,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate appsettings for luzlinks
- name: Generate appsettings for luslinks
run: |
cat > appsettings.luzlinks.json << 'CONFIG_EOF'
cat > appsettings.luslinks.json << 'CONFIG_EOF'
{
"Logging": {
"LogLevel": {
@ -524,18 +498,18 @@ jobs:
},
"SendGrid": {
"ApiKey": "${{ secrets.SENDGRID_API_KEY }}",
"FromEmail": "${{ vars.LUZLINKS_FROM_EMAIL || 'noreply@luzlinks.site' }}",
"FromName": "${{ vars.LUZLINKS_FROM_NAME || 'LuzLinks' }}"
"FromEmail": "${{ vars.LUSLINKS_FROM_EMAIL || 'noreply@luslinks.site' }}",
"FromName": "${{ vars.LUSLINKS_FROM_NAME || 'LusLinks' }}"
},
"Plans": {
"Basic": { "Name": "Básico", "PriceId": "price_1TR10UBk8jHwC3c0C9UJTNYg", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" },
"Professional": { "Name": "Profissional", "PriceId": "price_1TR10VBk8jHwC3c0v6nlFB0R", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" },
"Premium": { "Name": "Premium", "PriceId": "price_1TR10WBk8jHwC3c0QigqeR7b", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados" ], "Interval": "month" },
"PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1TR10XBk8jHwC3c03SfR3Z4v", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], "Interval": "month" },
"BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1TR10VBk8jHwC3c000wYtGxR", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], "Interval": "year" },
"ProfessionalYearly": { "Name": "Profissional Anual", "PriceId": "price_1TR10WBk8jHwC3c0zCitaA4j", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumYearly": { "Name": "Premium Anual", "PriceId": "price_1TR10XBk8jHwC3c0MSZFXh7x", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1TR10YBk8jHwC3c0EzYrA0PJ", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Economize R$ 59,80 (2 meses grátis)" ], "Interval": "year" }
"Basic": { "Name": "Básico", "PriceId": "price_1RycPaBMIadsOxJVKioZZofK", "Price": 12.90, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples" ], "Interval": "month" },
"Professional": { "Name": "Profissional", "PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj", "Price": 25.90, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado" ], "Interval": "month" },
"Premium": { "Name": "Premium", "PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu", "Price": 29.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados" ], "Interval": "month" },
"PremiumAffiliate": { "Name": "Premium+Afiliados", "PriceId": "price_1RycTaBMIadsOxJVeDLseXQq", "Price": 34.90, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados" ], "Interval": "month" },
"BasicYearly": { "Name": "Básico Anual", "PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS", "Price": 129.00, "MaxPages": 3, "MaxLinks": 8, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics simples", "Economize R$ 11,80 (2 meses grátis)" ], "Interval": "year" },
"ProfessionalYearly": { "Name": "Profissional Anual", "PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm", "Price": 259.00, "MaxPages": 5, "MaxLinks": 20, "AllowPremiumThemes": false, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": false, "MaxDocuments": 0, "Features": [ "URL Personalizada", "20 temas básicos", "Analytics avançado", "Economize R$ 25,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumYearly": { "Name": "Premium Anual", "PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m", "Price": 299.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": false, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 5, "Features": [ "URL Personalizada", "40 temas", "Suporte prioritário", "Links ilimitados", "Economize R$ 39,80 (2 meses grátis)" ], "Interval": "year" },
"PremiumAffiliateYearly": { "Name": "Premium+Afiliados Anual", "PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1", "Price": 349.00, "MaxPages": 15, "MaxLinks": -1, "AllowPremiumThemes": true, "AllowProductLinks": true, "AllowAnalytics": true, "AllowDocumentUpload": true, "MaxDocuments": 10, "Features": [ "Tudo do Premium", "Links de produto", "Moderação especial", "Até 10 links afiliados", "Economize R$ 59,80 (2 meses grátis)" ], "Interval": "year" }
},
"Moderation": {
"PriorityTimeframes": { "Trial": "7.00:00:00", "Basic": "7.00:00:00", "Professional": "3.00:00:00", "Premium": "1.00:00:00" },
@ -544,19 +518,19 @@ jobs:
"ModeratorEmails": [ "${{ vars.MODERATOR_EMAIL_1 || 'rrcgoncalves@gmail.com' }}", "${{ vars.MODERATOR_EMAIL_2 || 'rirocarneiro@gmail.com' }}" ]
},
"MongoDb": {
"ConnectionString": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/LuzLinksDB?replicaSet=rs0&authSource=admin",
"DatabaseName": "LuzLinksDB"
"ConnectionString": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/LusLinksDB?replicaSet=rs0&authSource=admin",
"DatabaseName": "LusLinksDB"
},
"BaseUrl": "https://luzlinks.site",
"BaseUrl": "https://luslinks.site",
"Tenant": {
"SiteName": "LuzLinks",
"SiteName": "LusLinks",
"SiteDescription": "A plataforma para pastores, padres, líderes religiosos e ministérios. Reúna seus estudos bíblicos, eventos, lives e canal em uma única página de fé.",
"Tagline": "Conecte sua comunidade em um único link",
"SupportEmail": "suporte@luzlinks.site",
"ContentFolder": "luzlinks",
"SupportEmail": "suporte@luslinks.site",
"ContentFolder": "luslinks",
"AgeGated": false,
"UrlExample": "luzlinks.site/pastor/seu-nome",
"DpoEmail": "dpo@luzlinks.site",
"UrlExample": "luslinks.site/pastor/seu-nome",
"DpoEmail": "dpo@luslinks.site",
"HeroHeadline": "Conecte sua comunidade em um único link",
"HeroDescription": "A plataforma ideal para pastores, padres, líderes e ministérios. Reúna seus estudos bíblicos, agenda de cultos, canal e dízimos em uma só página.",
"HeroCtaText": "Criar Minha Bio de Fé",
@ -567,37 +541,10 @@ jobs:
{ "Icon": "📅", "Title": "Agenda e Eventos", "Description": "Compartilhe retiros, cultos especiais e eventos com toda a comunidade de forma simples e organizada." }
],
"CtaHeadline": "Compartilhe sua mensagem com o mundo",
"CtaDescription": "Líderes de toda denominação já usam o LuzLinks para alcançar mais pessoas com sua mensagem de fé.",
"CtaDescription": "Líderes de toda denominação já usam o LusLinks para alcançar mais pessoas com sua mensagem de fé.",
"CtaButtonText": "Criar Minha Bio de Fé",
"MetaKeywords": "bio links pastor, página ministério, linktree cristão, links religiosos, página iglesia, bio pastor, links igreja",
"FooterTagline": "Conectando fé e comunidade.",
"HeroGradient": "linear-gradient(135deg, #5b9bd5 0%, #1a5276 100%)",
"PrimaryColor": "#2471a3",
"PrimaryColorDark": "#1a5276",
"DefaultCategories": [
{ "Icon": "🙏", "Name": "Pastores", "Slug": "pastor", "Description": "Pastores evangélicos, protestantes e pentecostais", "SeoKeywords": [ "pastor", "evangélico", "protestante", "pentecostal", "pregador" ] },
{ "Icon": "✝️", "Name": "Padres", "Slug": "padre", "Description": "Sacerdotes, padres e religiosos da Igreja Católica", "SeoKeywords": [ "padre", "sacerdote", "católico", "pároco", "religioso" ] },
{ "Icon": "⛪", "Name": "Igrejas", "Slug": "igreja", "Description": "Congregações, comunidades de fé e denominações", "SeoKeywords": [ "igreja", "congregação", "comunidade", "denominação", "templo" ] },
{ "Icon": "🌟", "Name": "Ministérios", "Slug": "ministerio", "Description": "Ministérios, organizações e missões cristãs", "SeoKeywords": [ "ministério", "missão", "organização cristã", "obra" ] },
{ "Icon": "🎵", "Name": "Louvor e Adoração", "Slug": "louvor", "Description": "Ministérios de louvor, bandas gospel e cantores cristãos", "SeoKeywords": [ "louvor", "adoração", "gospel", "banda", "música cristã" ] },
{ "Icon": "👨‍👩‍👧", "Name": "Família e Jovens", "Slug": "familia", "Description": "Líderes de grupos de jovens, casais e família", "SeoKeywords": [ "jovens", "família", "casais", "célula", "grupo" ] },
{ "Icon": "📖", "Name": "Estudos Bíblicos", "Slug": "estudos", "Description": "Mestres, professores bíblicos e teólogos", "SeoKeywords": [ "estudo bíblico", "teologia", "mestre", "professor", "bíblia" ] },
{ "Icon": "🌍", "Name": "Missionários", "Slug": "missionario","Description": "Missionários e evangelistas nacionais e internacionais", "SeoKeywords": [ "missionário", "evangelista", "evangelismo", "missões" ] },
{ "Icon": "📻", "Name": "Mídia Cristã", "Slug": "midia", "Description": "Podcasts, canais, rádios e comunicação cristã", "SeoKeywords": [ "podcast", "canal cristão", "rádio evangélica", "mídia" ] },
{ "Icon": "🤝", "Name": "Assistência Social", "Slug": "assistencia","Description": "Projetos sociais, pastorais de assistência e ONGs cristãs","SeoKeywords": [ "assistência social", "projeto social", "ONG", "pastoral" ] }
],
"AllowedLinkTypes": [
{ "Icon": "fas fa-globe", "Label": "🌐 Site / Ministério", "Prefix": "https://", "Placeholder": "ministerio.com.br", "Instructions": "Digite o domínio do site", "Color": "bg-primary" },
{ "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "contato@ministerio.com", "Instructions": "Digite apenas o email", "Color": "bg-success" },
{ "Icon": "fas fa-phone", "Label": "📞 Telefone / WhatsApp", "Prefix": "tel:", "Placeholder": "5511999999999", "Instructions": "Número com código do país", "Color": "bg-success" },
{ "Icon": "fab fa-youtube", "Label": "📺 YouTube", "Prefix": "https://youtube.com/", "Placeholder": "@canal ou c/CANAL", "Instructions": "Digite o canal ou @usuário", "Color": "bg-danger" },
{ "Icon": "fab fa-instagram", "Label": "📸 Instagram", "Prefix": "https://instagram.com/", "Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-danger" },
{ "Icon": "fas fa-book", "Label": "📖 Estudo / Série", "Prefix": "https://", "Placeholder": "link-do-estudo.com", "Instructions": "Link para estudo bíblico ou série", "Color": "bg-info" },
{ "Icon": "fas fa-calendar", "Label": "📅 Agenda / Eventos", "Prefix": "https://", "Placeholder": "calendly.com/seunome", "Instructions": "Link para agenda ou evento", "Color": "bg-warning" },
{ "Icon": "fas fa-donate", "Label": "🙏 Dízimos / Ofertas", "Prefix": "https://", "Placeholder": "pix.com.br/ministerio", "Instructions": "Link para doações ou dízimos", "Color": "bg-success" },
{ "Icon": "fas fa-map-marker-alt","Label": "📍 Localização", "Prefix": "https://maps.google.com/?q=", "VisualPrefix": "📍 Maps:", "Placeholder": "Rua da Igreja, 123", "Instructions": "Endereço da igreja/ministério", "Color": "bg-warning" },
{ "Icon": "fas fa-download", "Label": "⬇️ Material / Apostila","Prefix": "https://", "Placeholder": "drive.google.com/...", "Instructions": "Link para download de material", "Color": "bg-secondary" }
]
"FooterTagline": "Conectando fé e comunidade."
},
"Support": {
"TelegramUrl": "https://t.me/jobmakerbr",
@ -611,9 +558,9 @@ jobs:
}
}
CONFIG_EOF
echo "✅ appsettings.luzlinks.json gerado"
echo "✅ appsettings.luslinks.json gerado"
- name: Deploy luzlinks stack
- name: Deploy luslinks stack
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
@ -623,43 +570,36 @@ jobs:
# ── Sync Content to both nodes ──────────────────────────────────────
for NODE in ${{ env.SWARM_MANAGER }} ${{ env.SWARM_WORKER }}; do
echo "📂 Syncing luzlinks content to $NODE..."
ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/luzlinks && sudo chown -R ubuntu:ubuntu /opt/bcards-content'
rsync -az --delete -e "ssh -o StrictHostKeyChecking=no" src/BCards.Web/Content/Tenants/luzlinks/ ubuntu@$NODE:/opt/bcards-content/luzlinks/
echo "📂 Syncing luslinks content to $NODE..."
ssh -o StrictHostKeyChecking=no ubuntu@$NODE 'sudo mkdir -p /opt/bcards-content/luslinks && sudo chown -R ubuntu:ubuntu /opt/bcards-content'
scp -o StrictHostKeyChecking=no -r src/BCards.Web/Content/Tenants/luslinks/. ubuntu@$NODE:/opt/bcards-content/luslinks/
done
# ── Deploy stack on manager ─────────────────────────────────────────
scp -o StrictHostKeyChecking=no appsettings.luzlinks.json ubuntu@${{ env.SWARM_MANAGER }}:/tmp/
scp -o StrictHostKeyChecking=no deploy/docker-stack-luzlinks.yml ubuntu@${{ env.SWARM_MANAGER }}:/tmp/
scp -o StrictHostKeyChecking=no appsettings.luslinks.json ubuntu@${{ env.SWARM_MANAGER }}:/tmp/
scp -o StrictHostKeyChecking=no deploy/docker-stack-luslinks.yml ubuntu@${{ env.SWARM_MANAGER }}:/tmp/
ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} << 'EOF'
set -e
echo "🔄 Updating luzlinks stack..."
echo "🔄 Updating luslinks stack..."
# Remove stack luslinks legada se ainda existir
if docker stack ls --format '{{.Name}}' | grep -q '^luslinks$'; then
echo "🗑️ Removendo stack luslinks legada..."
docker stack rm luslinks
sleep 10
fi
docker config rm luslinks-appsettings 2>/dev/null || true
CONFIG_NAME="luslinks-appsettings-$(date +%s)"
docker config create ${CONFIG_NAME} /tmp/appsettings.luslinks.json
docker config rm luzlinks-appsettings 2>/dev/null || true
CONFIG_NAME="luzlinks-appsettings-$(date +%s)"
docker config create ${CONFIG_NAME} /tmp/appsettings.luzlinks.json
sed "s/luslinks-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack-luslinks.yml > /tmp/docker-stack-luslinks-final.yml
sed "s/luzlinks-appsettings/${CONFIG_NAME}/g" /tmp/docker-stack-luzlinks.yml > /tmp/docker-stack-luzlinks-final.yml
docker stack deploy -c /tmp/docker-stack-luslinks-final.yml luslinks --with-registry-auth
docker stack deploy -c /tmp/docker-stack-luzlinks-final.yml luzlinks --with-registry-auth
rm -f /tmp/appsettings.luzlinks.json /tmp/docker-stack-luzlinks.yml /tmp/docker-stack-luzlinks-final.yml
echo "✅ luzlinks stack atualizado!"
rm -f /tmp/appsettings.luslinks.json /tmp/docker-stack-luslinks.yml /tmp/docker-stack-luslinks-final.yml
echo "✅ luslinks stack atualizado!"
EOF
- name: Health Check luzlinks
- name: Health Check luslinks
run: |
sleep 30
ssh -o StrictHostKeyChecking=no ubuntu@${{ env.SWARM_MANAGER }} \
'curl -sf http://localhost:8083/health && echo "✅ luzlinks healthy" || echo "⚠️ luzlinks health check failed"'
'curl -sf http://localhost:8083/health && echo "✅ luslinks healthy" || echo "⚠️ luslinks health check failed"'
# ─── Release branch deploy (test swarm) ───────────────────────────────────
deploy-test:
@ -718,8 +658,8 @@ jobs:
cleanup:
name: Cleanup Old Resources
runs-on: ubuntu-latest
needs: [deploy-bcards, deploy-spicylinks, deploy-luzlinks, deploy-test]
if: always() && (needs.deploy-bcards.result == 'success' || needs.deploy-spicylinks.result == 'success' || needs.deploy-luzlinks.result == 'success' || needs.deploy-test.result == 'success')
needs: [deploy-bcards, deploy-spicylinks, deploy-luslinks, deploy-test]
if: always() && (needs.deploy-bcards.result == 'success' || needs.deploy-spicylinks.result == 'success' || needs.deploy-luslinks.result == 'success' || needs.deploy-test.result == 'success')
steps:
- name: Cleanup containers and images
@ -740,7 +680,7 @@ jobs:
docker image prune -f
docker network prune -f
# Remove stale swarm configs (keep last 3 per tenant)
for TENANT in bcards spicylinks luzlinks; do
for TENANT in bcards spicylinks luslinks; do
docker config ls --filter "name=${TENANT}-appsettings" --format "{{.ID}} {{.Name}}" \
| sort -k2 | head -n -3 | awk '{print $1}' \
| xargs -r docker config rm 2>/dev/null || true
@ -757,7 +697,7 @@ jobs:
deployment-summary:
name: Deployment Summary
runs-on: ubuntu-latest
needs: [deploy-bcards, deploy-spicylinks, deploy-luzlinks, deploy-test]
needs: [deploy-bcards, deploy-spicylinks, deploy-luslinks, deploy-test]
if: always()
steps:
@ -776,7 +716,7 @@ jobs:
echo ""
echo " bcards.site → :8080 [${{ needs.deploy-bcards.result }}]"
echo " spicylinks.site → :8082 [${{ needs.deploy-spicylinks.result }}]"
echo " luzlinks.site → :8083 [${{ needs.deploy-luzlinks.result }}]"
echo " luslinks.site → :8083 [${{ needs.deploy-luslinks.result }}]"
else
echo "🌍 Environment: Release (Test Swarm)"
echo "🔗 Status: ${{ needs.deploy-test.result }}"

View File

@ -244,45 +244,3 @@ if (page.Status == PageStatus.Creating || page.Status == PageStatus.Rejected)
```
This architecture supports a production-ready SaaS application with complex business rules, payment integration, and content moderation workflows.
# Project instructions
## ctx — use before reading files
This project has `ctx` available in PATH. Use it to understand the codebase **before** reading files directly. It produces compact markdown summaries that cost far fewer tokens than raw file content.
### When to use
**Always use `ctx` first** when you need to:
- Understand project structure → `ctx csharp project` or `ctx react project`
- Understand a file's structure → `ctx csharp outline <file>` or `ctx react outline <file>`
- Check build errors → `ctx csharp errors` or `ctx react errors`
- Understand git state → `ctx git`
- Detect what stack this project uses → `ctx auto detect`
### Workflow
1. **Start of session:** run `ctx auto detect` to see what's here, then `ctx <stack> project` for an overview.
2. **Before reading a file:** run `ctx <stack> outline <file>` first. Only read the full file if the outline isn't enough (e.g., you need to see method body logic).
3. **After making changes:** run `ctx <stack> errors` instead of `dotnet build` or `tsc` — the output is pre-filtered to only relevant diagnostics.
4. **Before committing:** run `ctx git` for a compact diff summary.
### Available commands
```
ctx auto detect # detect stack(s) in current directory
ctx auto project # run project summary for all detected stacks
ctx csharp project # .NET solution overview (projects, refs, packages)
ctx csharp outline <file.cs> # file structure without method bodies
ctx csharp errors # filtered dotnet build output (errors + top warnings)
ctx git # branch, status, recent commits, diff summary
```
### Important
- `ctx` output is a **summary**, not the full picture. If you need implementation details (method bodies, exact logic, specific lines), read the file directly.
- Do not run `ctx` commands that don't match the project stack. Use `ctx auto detect` if unsure.
- `ctx csharp errors` assumes `dotnet restore` was already run. If you get restore errors, run `dotnet restore` first, then `ctx csharp errors`.

View File

@ -1,59 +0,0 @@
# Plano de Novos Artigos
Data de criação: 2026-04-30
## LuzLinks — 4 artigos
- [x] **Como criar sua bio de fé do zero**
`Content/Tenants/luzlinks/Artigos/como-criar-sua-bio-de-fe-do-zero.pt-BR.md`
- [x] **Como divulgar cultos e eventos pelo WhatsApp com um único link**
`Content/Tenants/luzlinks/Artigos/como-divulgar-cultos-e-eventos-pelo-whatsapp.pt-BR.md`
- [x] **Por que pastores precisam de presença digital**
`Content/Tenants/luzlinks/Artigos/por-que-pastores-precisam-de-presenca-digital.pt-BR.md`
- [x] **Como receber dízimos online no seu ministério**
`Content/Tenants/luzlinks/Artigos/como-receber-dizimos-online-no-seu-ministerio.pt-BR.md`
## SpicyLinks — 4 artigos
- [x] **Como configurar sua bio para ganhar mais seguidores**
`Content/Tenants/spicylinks/Artigos/como-configurar-sua-bio-para-ganhar-mais-seguidores.pt-BR.md`
- [x] **Os melhores links para colocar na sua bio do Instagram**
`Content/Tenants/spicylinks/Artigos/os-melhores-links-para-sua-bio-do-instagram.pt-BR.md`
- [x] **Como criadores de conteúdo ganham dinheiro com links de afiliados**
`Content/Tenants/spicylinks/Artigos/como-criadores-de-conteudo-ganham-dinheiro-com-afiliados.pt-BR.md`
- [x] **SpicyLinks vs Linktree: qual é melhor para criadores?**
`Content/Tenants/spicylinks/Artigos/spicylinks-vs-linktree-qual-e-melhor-para-criadores.pt-BR.md`
---
## Melhorias de qualidade (SEO / estrutura)
### LuzLinks — aplicar em todos
- [ ] bio de fé do zero — "Para quem é" + prós/contras + Leia também
- [ ] WhatsApp + cultos — "Para quem é" + prós/contras + Leia também
- [ ] Pastores e digital — "Para quem é" + prós/contras + Leia também
- [ ] Dízimos online — "Para quem é" + prós/contras + Leia também
### SpicyLinks — aplicar em todos
- [ ] Bio e seguidores — "Para quem é" + prós/contras + Leia também
- [ ] Melhores links bio — "Para quem é" + prós/contras + Leia também
- [ ] Afiliados criadores — "Para quem é" + prós/contras + Leia também
- [ ] SpicyLinks vs Linktree — "Para quem é" + prós/contras + Leia também
### BCards — analisar e ajustar artigos existentes
- [ ] bcards-para-corretores-de-imoveis
- [ ] bcards-vs-linktree
- [ ] transformacao-digital-pequenos-negocios
- [ ] Tutoriais/advocacia/bcards-para-escritorios-de-advocacia
- [ ] Tutoriais/advocacia/como-advogados-podem-usar-bcards
- [ ] Tutoriais/tecnologia/bcards-para-freelancers-de-ti
- [ ] Tutoriais/tecnologia/como-criar-um-bcard

View File

@ -5,7 +5,7 @@ configs:
external: true
services:
bcards-app:
app:
image: registry.redecarneir.us/bcards:latest
networks:
- bcards-net

View File

@ -1,7 +1,7 @@
version: '3.8'
configs:
luzlinks-appsettings:
luslinks-appsettings:
external: true
services:
@ -23,20 +23,20 @@ services:
parallelism: 0
delay: 5s
configs:
- source: luzlinks-appsettings
- source: luslinks-appsettings
target: /app/appsettings.Production.json
mode: 0444
volumes:
- type: bind
source: /opt/bcards-content/luzlinks
target: /app/Content/Tenants/luzlinks
source: /opt/bcards-content/luslinks
target: /app/Content/Tenants/luslinks
read_only: true
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:8080
ASPNETCORE_FORWARDEDHEADERS_ENABLED: "true"
MongoDb__ConnectionString: mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/LuzLinksDB?replicaSet=rs0&authSource=admin
MongoDb__DatabaseName: LuzLinksDB
MongoDb__ConnectionString: mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/LusLinksDB?replicaSet=rs0&authSource=admin
MongoDb__DatabaseName: LusLinksDB
Serilog__OpenSearchUrl: http://141.148.162.114:19201
Serilog__OpenSearchFallback: http://129.146.116.218:19202
Logging__LogLevel__Default: Information

View File

@ -5,7 +5,7 @@
# Requires: certbot certificates for each domain
# certbot --nginx -d bcards.site -d www.bcards.site
# certbot --nginx -d spicylinks.site -d www.spicylinks.site
# certbot --nginx -d luzlinks.site -d www.luzlinks.site
# certbot --nginx -d luslinks.site -d www.luslinks.site
# ─── bcards.site → :8080 ───────────────────────────────────────────────────
@ -79,32 +79,32 @@ server {
}
}
# ─── luzlinks.site → :8083 ─────────────────────────────────────────────────
# ─── luslinks.site → :8083 ─────────────────────────────────────────────────
upstream luzlinks {
upstream luslinks {
server 127.0.0.1:8083;
keepalive 32;
}
server {
listen 80;
server_name luzlinks.site www.luzlinks.site;
server_name luslinks.site www.luslinks.site;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name luzlinks.site www.luzlinks.site;
server_name luslinks.site www.luslinks.site;
ssl_certificate /etc/letsencrypt/live/luzlinks.site/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/luzlinks.site/privkey.pem;
ssl_certificate /etc/letsencrypt/live/luslinks.site/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/luslinks.site/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
client_max_body_size 10M;
location / {
proxy_pass http://luzlinks;
proxy_pass http://luslinks;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;

BIN
saida.md

Binary file not shown.

View File

@ -1,14 +1,12 @@
@model List<BCards.Web.Areas.Tutoriais.Models.ArticleMetadata>
@inject Microsoft.Extensions.Options.IOptions<BCards.Web.Configuration.TenantSettings> TenantConfig
@{
var tenant = TenantConfig.Value;
ViewData["Title"] = $"Artigos {tenant.SiteName} - Inspiração e Conhecimento";
ViewData["Title"] = "Artigos BCards - Inspiração e Conhecimento";
}
<div class="container py-5">
<div class="row mb-5">
<div class="col-lg-8 mx-auto text-center">
<h1 class="display-4 mb-3">✨ Artigos @tenant.SiteName</h1>
<h1 class="display-4 mb-3">✨ Artigos BCards</h1>
<p class="lead text-muted">Insights, tendências e inspiração para transformar sua presença digital</p>
</div>
</div>
@ -22,7 +20,7 @@
<div class="card h-100 border-0 shadow-sm hover-shadow">
@if (!string.IsNullOrEmpty(artigo.Image))
{
<img src="@artigo.Image" class="card-img-top" alt="@artigo.Title" style="height: 200px; object-fit: cover;" onerror="this.remove()">
<img src="@artigo.Image" class="card-img-top" alt="@artigo.Title" style="height: 200px; object-fit: cover;">
}
<div class="card-body">
<h5 class="card-title">@artigo.Title</h5>
@ -57,7 +55,7 @@
<div class="card bg-primary text-white border-0 shadow">
<div class="card-body text-center p-5">
<h3 class="mb-3">Quer ver tutoriais práticos?</h3>
<p class="mb-4">Acesse nossa seção de tutoriais e aprenda passo a passo como usar o @tenant.SiteName</p>
<p class="mb-4">Acesse nossa seção de tutoriais e aprenda passo a passo como usar o BCards</p>
<a href="@Url.Action("Index", "Tutoriais", new { area = "Tutoriais" })" class="btn btn-light btn-lg">
<i class="fas fa-book-open me-2"></i> Ver Tutoriais
</a>

View File

@ -29,21 +29,4 @@ public class TenantSettings
// SEO / Layout
public string MetaKeywords { get; set; } = "cartão digital, página de links, bio links, linktree brasil, página profissional";
public string FooterTagline { get; set; } = "Sua presença online, simplificada.";
// Branding / Colors
public string HeroGradient { get; set; } = "linear-gradient(135deg, #667eea 0%, #764ba2 100%)";
public string PrimaryColor { get; set; } = "#0d6efd";
public string PrimaryColorDark { get; set; } = "#0a58ca";
// Category seeding (se vazio, usa os padrões do BCards)
public List<CategorySeedItem> DefaultCategories { get; set; } = new();
}
public class CategorySeedItem
{
public string Name { get; set; } = "";
public string Slug { get; set; } = "";
public string Icon { get; set; } = "";
public string Description { get; set; } = "";
public List<string> SeoKeywords { get; set; } = new();
}

View File

@ -2,7 +2,7 @@
title: "Como a Tecnologia Está Transformando o Ministério Moderno"
description: "Reflexão sobre como pastores, padres e líderes religiosos podem usar ferramentas digitais para alcançar mais pessoas com a mensagem de fé, sem perder a essência do ministério."
keywords: "ministério digital, igreja online, pastor digital, tecnologia e fé, evangelismo digital"
author: "Equipe LuzLinks"
author: "Equipe LusLinks"
date: 2026-01-28
lastMod: 2026-01-28
image: "/images/artigos/ministerio-digital.jpg"
@ -112,7 +112,7 @@ Sem uma centralização clara, o esforço de presença digital se fragmenta.
É por isso que a bio link — uma página única com todos os contatos e plataformas do ministério — se tornou uma necessidade básica para líderes que levam a sério a presença digital.
Com uma boa página no LuzLinks, qualquer pessoa que encontra o ministério em qualquer plataforma chega ao mesmo lugar: uma porta de entrada organizada, com links para as lives, o grupo da comunidade, a agenda, e como contribuir com o ministério.
Com uma boa página no LusLinks, qualquer pessoa que encontra o ministério em qualquer plataforma chega ao mesmo lugar: uma porta de entrada organizada, com links para as lives, o grupo da comunidade, a agenda, e como contribuir com o ministério.
---
@ -151,9 +151,9 @@ O desafio não é técnico. É espiritual: usar bem o que temos disponível, sem
---
**Quer centralizar a presença digital do seu ministério?**
Crie sua página no LuzLinks e ofereça à sua congregação uma porta de entrada organizada para tudo que você produz.
Crie sua página no LusLinks e ofereça à sua congregação uma porta de entrada organizada para tudo que você produz.
[Criar minha página no LuzLinks →](https://luzlinks.site/)
[Criar minha página no LusLinks →](https://luslinks.site/)
---

View File

@ -1,18 +1,18 @@
---
title: "Como Criar sua Página no LuzLinks: Guia Completo para Líderes Religiosos"
description: "Passo a passo para pastores, padres, líderes e ministérios criarem sua página profissional no LuzLinks e alcançarem mais pessoas com sua mensagem de fé."
keywords: "luzlinks tutorial, página pastor, bio links ministerio, presença digital iglesia, como criar página pastor"
author: "Equipe LuzLinks"
title: "Como Criar sua Página no LusLinks: Guia Completo para Líderes Religiosos"
description: "Passo a passo para pastores, padres, líderes e ministérios criarem sua página profissional no LusLinks e alcançarem mais pessoas com sua mensagem de fé."
keywords: "luslinks tutorial, página pastor, bio links ministerio, presença digital iglesia, como criar página pastor"
author: "Equipe LusLinks"
date: 2026-01-25
lastMod: 2026-01-25
image: "/images/tutoriais/pastor-luzlinks.jpg"
image: "/images/tutoriais/pastor-luslinks.jpg"
culture: "pt-BR"
category: "ministerio"
---
# Como Criar sua Página no LuzLinks: Guia Completo para Líderes Religiosos
# Como Criar sua Página no LusLinks: Guia Completo para Líderes Religiosos
Sua congregação está em vários lugares ao mesmo tempo: no Instagram, no YouTube, no WhatsApp, no Telegram, na agenda de eventos — e você fica repetindo os mesmos links toda semana. O LuzLinks resolve isso com uma página única que reúne tudo.
Sua congregação está em vários lugares ao mesmo tempo: no Instagram, no YouTube, no WhatsApp, no Telegram, na agenda de eventos — e você fica repetindo os mesmos links toda semana. O LusLinks resolve isso com uma página única que reúne tudo.
Este guia mostra o passo a passo completo para líderes religiosos criarem sua bio de fé.
@ -35,11 +35,11 @@ Quando alguém perguntar "onde acompanho o ministério?", você precisa ter uma
---
## Passo 1: Criar sua Conta no LuzLinks
## Passo 1: Criar sua Conta no LusLinks
### 1.1. Acesse o Site
Vá para [luzlinks.site](https://luzlinks.site) e clique em **"Entrar"** no menu.
Vá para [luslinks.site](https://luslinks.site) e clique em **"Entrar"** no menu.
### 1.2. Login Social (Recomendado)
@ -68,17 +68,17 @@ O nome deve ser claro e reconhecível pela sua congregação:
### Escolhendo o Slug (URL)
Sua URL no LuzLinks seguirá o padrão:
Sua URL no LusLinks seguirá o padrão:
```
luzlinks.site/ministerio/seu-slug
luslinks.site/ministerio/seu-slug
```
Boas opções:
```
pr-joao-silva → luzlinks.site/ministerio/pr-joao-silva
igreja-graca-viva → luzlinks.site/ministerio/igreja-graca-viva
ministerio-agape → luzlinks.site/ministerio/ministerio-agape
pe-carlos-mendes → luzlinks.site/ministerio/pe-carlos-mendes
pr-joao-silva → luslinks.site/ministerio/pr-joao-silva
igreja-graca-viva → luslinks.site/ministerio/igreja-graca-viva
ministerio-agape → luslinks.site/ministerio/ministerio-agape
pe-carlos-mendes → luslinks.site/ministerio/pe-carlos-mendes
```
> **Dica:** Escolha algo simples que qualquer membro consiga digitar de memória.
@ -171,7 +171,7 @@ URL: mailto:secretaria@suaigreja.com.br
## Passo 4: Escolhendo o Tema Visual
O LuzLinks oferece temas adequados para conteúdo espiritual. Recomendamos:
O LusLinks oferece temas adequados para conteúdo espiritual. Recomendamos:
- **Clássico ou Minimalista** para igrejas tradicionais (católicas, luteranas, presbiterianas)
- **Gradiente suave ou Azul** para igrejas evangélicas pentecostais
@ -203,7 +203,7 @@ Com a página aprovada, divulgue por todos os canais:
Projete no telão durante os avisos:
```
"Encontre todos os nossos links e contatos em:
luzlinks.site/ministerio/sua-church"
luslinks.site/ministerio/sua-church"
```
### No Instagram
@ -212,7 +212,7 @@ Atualize a bio:
```
Igreja Graça Viva | São Paulo
Cultos: Dom 9h e 19h | Qua 20h
🔗 Todos os links: luzlinks.site/ministerio/igreja-graca-viva
🔗 Todos os links: luslinks.site/ministerio/igreja-graca-viva
```
### No WhatsApp dos Membros
@ -223,7 +223,7 @@ Irmãos, agora temos uma página única com todos os nossos links:
cultos, agenda, YouTube, dízimos e mais.
👇 Salve e compartilhe:
luzlinks.site/ministerio/igreja-graca-viva
luslinks.site/ministerio/igreja-graca-viva
```
### No YouTube
@ -231,7 +231,7 @@ luzlinks.site/ministerio/igreja-graca-viva
Fixe o link como comentário pinado nos vídeos e inclua na descrição:
```
Todos os nossos contatos e próximos eventos:
luzlinks.site/ministerio/sua-church
luslinks.site/ministerio/sua-church
```
---
@ -261,7 +261,7 @@ Sim! Com o plano Premium você pode ter múltiplas páginas. Muitos líderes tê
Sempre que renovar o link de convite, acesse o Dashboard e atualize o link na sua página. Leva menos de 1 minuto.
**Posso colocar link de canal privado (assinatura)?**
Sim, desde que o conteúdo seja compatível com a plataforma. O LuzLinks é focado em conteúdo espiritual e educativo.
Sim, desde que o conteúdo seja compatível com a plataforma. O LusLinks é focado em conteúdo espiritual e educativo.
**Posso ter a página em português e espanhol?**
Para congregações bilíngues, recomendamos duas páginas: uma em pt-BR e outra em es. O plano Premium permite isso.
@ -270,7 +270,7 @@ Para congregações bilíngues, recomendamos duas páginas: uma em pt-BR e outra
## Conclusão
Criar sua página no LuzLinks é o primeiro passo para organizar a presença digital do seu ministério. Em vez de repetir links toda semana, você compartilha uma URL simples que sua congregação encontra tudo.
Criar sua página no LusLinks é o primeiro passo para organizar a presença digital do seu ministério. Em vez de repetir links toda semana, você compartilha uma URL simples que sua congregação encontra tudo.
**Recapitulando:**
1. ✅ Criar conta com email do ministério
@ -280,7 +280,7 @@ Criar sua página no LuzLinks é o primeiro passo para organizar a presença dig
5. ✅ Submeter para moderação
6. ✅ Divulgar para a congregação
[Criar minha página de fé →](https://luzlinks.site/)
[Criar minha página de fé →](https://luslinks.site/)
---

View File

@ -1,190 +0,0 @@
---
title: "Como Criar Sua Bio de Fé do Zero"
description: "Um guia prático para pastores, líderes e ministérios que querem criar uma página de links organizada, bonita e que realmente represente o trabalho do ministério online."
keywords: "bio link pastor, como criar pagina ministerio, presenca digital igreja, links para pastores, ministerio digital"
author: "Equipe LuzLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/bio-de-fe.jpg"
culture: "pt-BR"
category: "ministerio"
---
# Como Criar Sua Bio de Fé do Zero
Você já precisou enviar vários links diferentes para alguém que queria saber mais sobre o seu ministério? Link do YouTube, link do grupo do WhatsApp, link da agenda, Pix para dízimos… cada coisa num lugar diferente.
Existe uma forma muito mais simples de resolver isso: uma bio de links. Uma página única que reúne tudo que seu ministério oferece online, pronta para ser compartilhada com um só endereço.
Este guia mostra como criar a sua do zero, mesmo que você não tenha experiência com tecnologia.
---
## Este artigo é para você se...
- Você é pastor, padre, líder ou membro ativo de um ministério
- Você manda links diferentes para a congregação e quer centralizar tudo num único endereço
- Você ainda não tem nenhuma bio de links — ou tem uma desatualizada
- Você não precisa de experiência técnica: tudo aqui é feito pelo celular ou computador, sem programação
**O que você encontra aqui:** como escolher e organizar os links do ministério, o que escrever na descrição, erros comuns para evitar e o passo a passo completo para publicar.
---
## O que é uma bio de links?
Uma bio de links é uma página simples na internet com o seu nome, uma foto, uma descrição breve e uma lista de links para tudo que você quer que as pessoas encontrem.
Funciona assim: você cria uma página no LuzLinks, configura seus links, e recebe um endereço único — como `luzlinks.site/pastor/seu-nome`. Esse endereço vai na bio do Instagram, no perfil do WhatsApp, no final de cada vídeo do YouTube, em qualquer lugar que você queira.
Quem clicar chega numa página organizada com acesso a tudo.
---
## Antes de criar: o que você precisa ter em mãos
Antes de começar, separe estas informações:
**Foto de perfil**
Use uma foto de rosto clara, com boa iluminação. Não precisa ser profissional, mas precisa ser nítida. Uma foto em frente a uma janela iluminada já resolve bem.
**Nome do ministério ou do pastor**
Decida se vai usar seu nome pessoal, o nome da igreja ou o nome do ministério. Se você tem ambos, pode colocar "Pastor João Silva — Igreja Esperança".
**Descrição breve (até 2 linhas)**
O que você faz? Para quem? Onde?
Exemplo: *"Pastor evangélico em São Paulo. Pregações, estudos bíblicos e agenda de cultos."*
**Lista de links que você quer incluir**
Anote todos os links que você usa. Depois você vai organizar por prioridade.
---
## Quais links colocar (e em que ordem)
A ordem importa. As pessoas clicam mais nos primeiros links, então coloque o mais importante no topo.
### Sugestão de ordem para ministérios:
**1. Canal do YouTube**
Se você posta pregações ou estudos bíblicos, este é o link mais valioso. Uma pessoa que acabou de descobrir seu ministério vai querer assistir antes de qualquer outra coisa.
**2. Grupo do WhatsApp ou Telegram**
A comunidade da sua congregação fica aqui. Facilita muito para novos interessados entrarem em contato.
**3. Agenda de cultos ou eventos**
Um link direto para onde você divulga os próximos eventos — pode ser uma página do Instagram, um Google Agenda público ou um link simples.
**4. Dízimos e doações**
Se o seu ministério recebe dízimos ou ofertas pelo Pix, inclua um link aqui. Pode ser uma página de doação, um link do Vakinha, ou um link que abre o Pix no celular.
**5. Instagram**
Para quem quer acompanhar o dia a dia do ministério.
**6. Site da igreja (se tiver)**
Informações mais completas, histórico, endereço físico, horários fixos.
**7. Material para download (se tiver)**
Apostilas, hinários, materiais de célula — tudo que você distribui pode ter um link direto.
---
## Como escrever a descrição da sua página
Muitos líderes escrevem uma descrição vaga como "Ministério da Graça". Isso diz muito pouco para quem ainda não te conhece.
Uma boa descrição responde três perguntas rápidas:
- Quem é você?
- O que você oferece?
- Onde/como a pessoa pode participar?
**Exemplos práticos:**
*Versão genérica (evitar):*
> "Ministério da Graça — Servindo ao Senhor"
*Versão mais eficaz:*
> "Pastor João Silva — Pregações toda semana no YouTube. Cultos às quartas e domingos em São Paulo. Comunidade aberta no WhatsApp."
A segunda versão faz a pessoa entender em segundos se quer continuar explorando ou não.
---
## Dicas para a foto e aparência da página
**Foto de perfil:** Olhos nos olhos com a câmera, expressão acolhedora, fundo neutro ou de preferência relacionado ao ministério (púlpito, biblia, bandeira da igreja).
**Tema visual:** Escolha um tema que combine com a identidade do ministério. Tons de azul e branco transmitem paz e serenidade. Tons de dourado e vinho remetem a tradição e reverência. Evite cores muito agitadas — simplicidade passa mais confiança.
**Nome na página:** Use o nome pelo qual você é conhecido, não necessariamente o nome legal. "Pastor João" pode ser mais reconhecível do que "João Ferreira da Silva".
---
## Erros comuns ao criar uma bio de fé
**Colocar links quebrados**
Antes de publicar, clique em cada link para confirmar que está funcionando. Links quebrados afastam as pessoas.
**Usar descrição muito longa**
A bio não é um sermão. Duas ou três linhas são o suficiente. Quem quiser saber mais vai clicar nos links.
**Não atualizar quando algo muda**
Se você cria um novo grupo do WhatsApp, muda o canal do YouTube ou abre um novo site, lembre de atualizar a bio também.
**Ignorar a foto de perfil**
Uma página sem foto parece abandonada. Mesmo que seja uma imagem simples, coloque uma foto.
---
## Passo a passo para criar no LuzLinks
1. Acesse [luzlinks.site](https://luzlinks.site/) e crie sua conta com email ou Google
2. Clique em "Criar nova página"
3. Escolha a categoria que melhor representa seu ministério (pastor, padre, líder, etc.)
4. Defina sua URL personalizada — ex: `luzlinks.site/pastor/joao-silva`
5. Adicione foto de perfil e descrição
6. Adicione seus links na ordem de prioridade
7. Escolha um tema visual
8. Envie para moderação e aguarde a aprovação
Depois de aprovada, compartilhe o link em todas as suas redes e coloque na bio do Instagram e no status do WhatsApp.
---
## Por onde começar hoje?
Se você está começando do zero, não espere ter tudo pronto. Comece com o básico:
- Foto de perfil
- Descrição de duas linhas
- Três ou quatro links principais
Depois você vai ajustando conforme usa. Uma página simples e funcionando é muito melhor do que uma página perfeita que nunca sai do papel.
---
## O que pode não funcionar
- **Sem foto de perfil:** páginas sem foto têm taxa de saída muito mais alta — as pessoas não confiam num perfil anônimo
- **Links quebrados:** um link que não abre faz a pessoa desistir e não voltar; cheque tudo antes de publicar
- **Descrição genérica:** "Ministério da Graça" sem contexto não converte — seja específico sobre o que você oferece
- **Página desatualizada:** um horário de culto errado ou um grupo lotado (sem acesso) frustra quem quer participar
---
## Leia também
- [Como Divulgar Cultos e Eventos pelo WhatsApp com um Único Link](/artigos/como-divulgar-cultos-e-eventos-pelo-whatsapp)
- [Por Que Pastores Precisam de Presença Digital](/artigos/por-que-pastores-precisam-de-presenca-digital)
- [Como Receber Dízimos Online no Seu Ministério](/artigos/como-receber-dizimos-online-no-seu-ministerio)
---
**Pronto para centralizar a presença digital do seu ministério?**
Crie sua página no LuzLinks e ofereça à sua congregação um único link para tudo.
[Criar minha bio de fé →](https://luzlinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -1,189 +0,0 @@
---
title: "Como Divulgar Cultos e Eventos pelo WhatsApp com um Único Link"
description: "Chega de mandar vários links diferentes no grupo da igreja. Veja como centralizar a agenda, o link da live e as informações do culto em uma única página que você compartilha com facilidade."
keywords: "divulgar culto whatsapp, agenda igreja online, link ministerio whatsapp, como divulgar eventos religiosos, bio link pastor whatsapp"
author: "Equipe LuzLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/cultos-whatsapp.jpg"
culture: "pt-BR"
category: "ministerio"
---
# Como Divulgar Cultos e Eventos pelo WhatsApp com um Único Link
Todo líder religioso que usa o WhatsApp para se comunicar com a congregação conhece bem essa situação: é véspera de culto, você precisa mandar o lembrete para o grupo, mas junto precisa enviar o link da live, o endereço no Google Maps, o link para o pedido de oração e talvez o Pix para a oferta especial.
Resultado: uma mensagem enorme, cheia de links, que a maioria das pessoas simplesmente para de ler no meio.
Existe uma solução bem mais simples para isso.
---
## Este artigo é para você se...
- Você já usa grupos de WhatsApp para se comunicar com a congregação
- Você manda vários links separados nos grupos e sente que as pessoas se perdem
- Você quer que um novo contato encontre tudo em um só lugar, sem precisar perguntar
- Você quer economizar tempo na comunicação semanal do ministério
**O que você encontra aqui:** como estruturar a bio de links para eventos, quando atualizar o link da live, modelos de mensagem prontos para usar e como aplicar o mesmo link em outros canais.
---
## O problema com múltiplos links no WhatsApp
Quando você manda vários links numa mensagem, acontece algo previsível: as pessoas abrem um ou dois, ignoram o resto, e ficam sem as informações que precisariam ter visto.
Isso não é falta de atenção da congregação. É assim que as pessoas funcionam com mensagens longas — especialmente em grupos com muitas notificações.
Além disso, toda vez que algo muda (nova live, novo link do Zoom, endereço diferente), você precisa mandar tudo de novo. O grupo começa a ficar poluído com mensagens repetidas.
---
## A solução: um único link que tem tudo
Com uma página no LuzLinks, você cria um endereço único — como `luzlinks.site/pastor/seu-nome` — que concentra tudo que a congregação precisa saber.
A mensagem do grupo vira algo assim:
> "Culto de quarta à noite às 19h. Acesse aqui todos os links: luzlinks.site/pastor/joao-silva"
Uma linha. Um link. Tudo que a pessoa precisa está lá quando ela clicar.
---
## Como estruturar a página para divulgação de eventos
O segredo está em como você organiza os links. Para uma congregação que acompanha eventos regularmente, esta ordem funciona bem:
**1. Link da live (transmissão ao vivo)**
Esse é o mais urgente e deve ficar no topo. Quando tem culto ao vivo, a pessoa que chega tarde ou não pôde ir precisa encontrar esse link rápido.
Dica: se o link da live muda toda semana (como acontece com algumas plataformas), use um link que aponte para a playlist do canal — assim você não precisa atualizar a bio a cada culto.
**2. Agenda de cultos**
Um link para onde está a programação completa. Pode ser uma nota no Instagram, um post fixado, ou uma página do Google Sites com os horários.
**3. Endereço da igreja (Google Maps)**
Para quem vai presencialmente. O link do Maps abre direto no aplicativo de navegação do celular.
**4. Grupo do WhatsApp principal**
Para novos membros que chegaram pelo link e ainda não estão no grupo.
**5. Pedidos de oração**
Um formulário simples (Google Forms funciona muito bem) ou um link direto para mensagem no WhatsApp.
**6. Dízimos e ofertas**
Pix, link de doação ou a plataforma que seu ministério usa.
**7. Canal do YouTube**
Para quem quer ver pregações anteriores.
---
## Quando atualizar o link da live
A maior dúvida de quem usa essa estratégia é: "E quando o link da live muda?"
**Se você transmite pelo YouTube:** O link do canal não muda nunca. Ao invés de linkar uma live específica, link o canal. Quem acessa vai ver a live no topo automaticamente quando ela estiver acontecendo.
**Se você transmite pelo Zoom:** O ID da reunião costuma ser sempre o mesmo quando você agenda recorrente. Verifique nas configurações do Zoom se pode usar "ID de reunião pessoal" — assim o link nunca muda.
**Se você transmite pelo Instagram Live:** Infelizmente não existe link direto para lives ao vivo no Instagram. Nesse caso, link seu perfil do Instagram e oriente a congregação a abrir o app e ir direto ao seu perfil.
---
## Modelo de mensagem para o grupo
Com a bio montada, você pode usar sempre a mesma estrutura de mensagem:
---
*Modelo para culto semanal:*
> 📅 Culto de quarta — hoje às 19h
>
> ▶️ Live: [link direto caso seja específico]
> 📍 Presencial: [endereço]
>
> Tudo em um só lugar: luzlinks.site/pastor/seu-nome
---
*Modelo para evento especial:*
> 🙏 Retiro de jovens — sábado, 10 de maio
>
> Informações completas, como chegar e inscrições:
> luzlinks.site/pastor/seu-nome
---
Perceba que a mensagem é curta. O link faz o trabalho pesado.
---
## Coloque o link no status do WhatsApp
Além do grupo, coloque o link da sua bio no status do WhatsApp. O status fica visível por 24 horas para todos os seus contatos, não apenas para quem está no grupo da igreja.
Isso alcança pessoas que você conhece pessoalmente mas que ainda não fazem parte da congregação — um contato de trabalho, um familiar, um vizinho que você encontra raramente.
Uma mensagem simples no status:
> "Culto hoje às 19h — link de tudo aqui 👆 luzlinks.site/pastor/seu-nome"
---
## Usando o link em outros lugares
O mesmo link que você usa no WhatsApp funciona em qualquer lugar:
**Bio do Instagram:** cole na bio e sempre que fizer stories peça para a pessoa "clicar no link da bio"
**Cartões de visita:** se você distribui cartões físicos, o link da bio pode ser o único endereço que você precisa colocar
**Assinatura de email:** coloque no final dos seus emails como "Acesse minha agenda: luzlinks.site/pastor/seu-nome"
**Final de pregações gravadas:** ao editar os vídeos, inclua o link nas telas finais e na descrição do YouTube
---
## Resumo prático
A bio de links não resolve apenas o problema do WhatsApp. Ela resolve o problema de ter presença digital fragmentada — cada plataforma com uma informação diferente, a congregação sem saber onde olhar.
Com um único link atualizado, você:
- Manda mensagens mais curtas e eficazes
- Facilita para pessoas novas encontrarem tudo de uma vez
- Reduz as mensagens repetidas nos grupos
- Tem um único lugar para atualizar quando algo muda
---
## O que pode não funcionar
- **Link da live que muda toda semana:** se você precisa atualizar o link da bio a cada culto, considere linkar o canal do YouTube ao invés da live específica — o canal sempre mostra a transmissão ativa no topo
- **Grupos lotados no WhatsApp:** se seu grupo atingiu o limite de participantes, o link de convite para e funcionar; mantenha a bio atualizada com o link do grupo atual
- **Bio usada apenas no grupo principal:** o maior potencial do link único é alcançar pessoas *fora* do grupo já existente — divulgue também no status do WhatsApp, no Instagram e em outras redes
- **Página sem agenda atualizada:** uma agenda desatualizada (com cultos que já passaram) confunde quem está tentando participar pela primeira vez
---
## Leia também
- [Como Criar Sua Bio de Fé do Zero](/artigos/como-criar-sua-bio-de-fe-do-zero)
- [Como Receber Dízimos Online no Seu Ministério](/artigos/como-receber-dizimos-online-no-seu-ministerio)
- [Por Que Pastores Precisam de Presença Digital](/artigos/por-que-pastores-precisam-de-presenca-digital)
---
**Quer configurar sua página e simplificar a comunicação com a congregação?**
Crie sua bio de fé no LuzLinks e comece a usar um link para tudo.
[Criar minha página →](https://luzlinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -1,177 +0,0 @@
---
title: "Como Receber Dízimos Online no Seu Ministério"
description: "Um guia prático sobre as opções disponíveis para receber dízimos, ofertas e doações online — com foco em segurança, transparência e facilidade para a congregação."
keywords: "dizimo online, receber oferta online, pix igreja, doacao ministerio, contribuicao igreja digital"
author: "Equipe LuzLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/dizimos-online.jpg"
culture: "pt-BR"
category: "ministerio"
---
# Como Receber Dízimos Online no Seu Ministério
A oferta no culto presencial tem um significado especial — é um gesto de fé, um ato de adoração feito em comunidade. Isso não muda com o digital.
Mas a realidade é que cada vez menos pessoas carregam dinheiro em espécie. E membros que assistem remotamente, que estão viajando, ou que simplesmente esqueceram o envelope em casa, podem querer contribuir de outras formas.
Disponibilizar opções de contribuição online não é substituir a oferta presencial — é incluir mais pessoas na prática.
---
## Este artigo é para você se...
- Você lidera um ministério e quer facilitar a contribuição de membros que assistem remotamente
- Você percebe que parte da congregação não contribui simplesmente porque não carrega dinheiro em espécie
- Você quer saber quais ferramentas existem no Brasil para receber contribuições online, com custo baixo ou zero
- Você quer fazer isso com transparência e sem complicar a gestão financeira do ministério
**O que você encontra aqui:** as principais opções de recebimento digital disponíveis no Brasil (Pix, plataformas de doação, internacionais), como comunicar isso à congregação e boas práticas de transparência financeira.
---
## Por que oferecer opções digitais de dízimo
**Membros remotos também querem contribuir**
Quem acompanha o ministério pelo YouTube ou vive em outra cidade frequentemente não tem como participar da oferta presencial. Uma forma digital de contribuir mantém esse vínculo.
**A geração mais jovem não usa dinheiro em espécie**
Pessoas entre 20 e 35 anos raramente têm notas no bolso. Se a única forma de contribuir é com dinheiro físico, essa parte da congregação simplesmente não contribui — não por falta de vontade, mas por falta de meio.
**Maior regularidade nas contribuições**
Uma pessoa que configura um Pix recorrente ou uma assinatura de contribuição tende a ser mais consistente do que uma que depende de ter dinheiro em mãos todo domingo.
**Facilidade de prestação de contas**
Contribuições digitais geram registro automático, o que facilita a gestão financeira e a transparência com a congregação.
---
## As principais opções disponíveis no Brasil
### Pix
O Pix é a opção mais simples e com menos burocracia. Qualquer banco ou fintech oferece gratuitamente, e a transferência é imediata.
**Como disponibilizar:**
- Crie uma chave Pix com um email ou número dedicado ao ministério (diferente do pessoal)
- Gere um QR Code estático de cobrança — sem valor fixo, a pessoa decide o valor
- Coloque o QR Code nos cultos, nos slides de apresentação, e o link na sua bio do LuzLinks
**Vantagem:** sem taxa, imediato, qualquer pessoa com app de banco consegue usar
**Desvantagem:** não tem gestão automática de recorrência — é uma transferência avulsa cada vez
---
### Conta bancária para crédito (TED/DOC)
Para dízimos maiores ou pessoas mais acostumadas com transferência bancária tradicional, ter os dados bancários disponíveis é importante.
Mantenha esses dados em lugar acessível — na bio do LuzLinks, no site da igreja, nos grupos do WhatsApp.
---
### Plataformas de doação recorrente
Para ministérios que querem gestão mais profissional de contribuições:
**Vakinha**
Focado em campanhas pontuais (reforma da sede, compra de equipamento, projeto especial). Ótimo para arrecadações com objetivo definido.
**PagSeguro / Mercado Pago**
Permitem criar links de doação com ou sem valor fixo, e configurar doações recorrentes. Cobram uma taxa por transação (em torno de 3-5%).
**Gerencianet / Efí**
Plataformas financeiras brasileiras que oferecem cobranças recorrentes via Pix ou boleto. Mais adequadas para ministérios que querem estrutura financeira mais completa.
**Assinaturas pelo WhatsApp/Telegram**
Algumas igrejas criam grupos "de membros contribuintes" onde a contribuição é o critério de participação — combinando comunicação especial com a prática do dízimo.
---
### Para contribuições internacionais
Se você tem membros ou doadores no exterior, o PayPal e o Wise são opções viáveis. Ambos permitem receber transferências de outros países com taxas razoáveis.
---
## Como comunicar as opções de forma eficaz
Disponibilizar as formas de contribuição não é suficiente — você precisa comunicar com clareza e naturalidade.
**No culto presencial:**
Reserve um momento breve para mostrar o QR Code do Pix no telão. Uma frase simples funciona bem: *"Para quem prefere contribuir pelo celular, o Pix está disponível aqui."*
**Nas transmissões ao vivo:**
Mencione as formas de contribuição no início e no final da transmissão. Não precisa ser uma campanha — uma referência breve e direta é suficiente.
**Na sua bio do LuzLinks:**
Inclua um link de dízimos na sua página. Algo como "Dízimos e Ofertas" com um link para o Pix ou para a página de doação. Quando alguém descobre seu ministério e quer contribuir, esse link é o caminho mais direto.
**No grupo do WhatsApp:**
Fixe uma mensagem com todas as formas de contribuição para que novos membros consigam encontrar facilmente.
---
## Transparência: a base da confiança financeira
Independente do método usado, a transparência é o que constrói e mantém a confiança da congregação nas finanças do ministério.
**Boas práticas:**
- Apresente um relatório financeiro periódico para a congregação (mensalmente ou trimestralmente)
- Explique para onde vai o dinheiro das ofertas e dízimos
- Tenha mais de uma pessoa responsável pela gestão financeira — nunca uma pessoa só
- Separe contas: conta pessoal do pastor e conta do ministério devem ser sempre diferentes
Ministérios que tratam as finanças com transparência raramente enfrentam crises de confiança. E transparência é mais fácil quando as contribuições são digitais, porque o registro é automático e consultável.
---
## Um sistema simples que funciona
Para a maioria dos ministérios, este conjunto básico já atende bem:
1. **Pix com QR Code** para contribuições espontâneas no culto e online
2. **Link do Pix ou página de doação** na bio do LuzLinks
3. **Menção breve nos cultos** sobre as formas disponíveis
4. **Relatório financeiro mensal** compartilhado com a liderança e, em versão resumida, com a congregação
Você pode começar com isso hoje, sem custo e sem complicação.
---
## Sobre a parte espiritual
Este artigo focou nos aspectos práticos — ferramentas, plataformas, comunicação. Mas é importante lembrar que o dízimo e a oferta têm um significado espiritual que vai além da logística.
A facilidade de contribuição digital deve ser uma ferramenta a serviço da prática de fé, não uma substituição do seu significado. O que você ensina sobre mordomia e contribuição é tão importante quanto as ferramentas que você disponibiliza.
Use o digital para remover obstáculos práticos — não para transformar a oferta em algo mecânico.
---
## O que pode não funcionar
- **Usar apenas o Pix pessoal do pastor:** misturar finanças pessoais e do ministério é a raiz de muitos problemas de confiança — crie um email e chave Pix separados para o ministério
- **Não comunicar as formas disponíveis:** disponibilizar sem avisar é como ter uma porta aberta num quarto escuro; mencione regularmente nos cultos e nas transmissões
- **Depender de uma única plataforma:** plataformas mudam políticas e taxas; ter Pix como opção principal e uma segunda alternativa garante continuidade
- **Não prestar contas:** contribuições digitais sem prestação de contas periódica geram desconfiança; um relatório simples, mesmo que informal, faz diferença
---
## Leia também
- [Como Criar Sua Bio de Fé do Zero](/artigos/como-criar-sua-bio-de-fe-do-zero)
- [Como Divulgar Cultos e Eventos pelo WhatsApp com um Único Link](/artigos/como-divulgar-cultos-e-eventos-pelo-whatsapp)
- [Por Que Pastores Precisam de Presença Digital](/artigos/por-que-pastores-precisam-de-presenca-digital)
---
**Coloque o link dos dízimos na sua bio de fé.**
Com uma página no LuzLinks, sua congregação encontra todas as formas de contribuição em um só lugar.
[Criar minha página no LuzLinks →](https://luzlinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -1,168 +0,0 @@
---
title: "Por Que Pastores Precisam de Presença Digital"
description: "Uma análise honesta de por que a presença digital deixou de ser opcional para líderes religiosos — e como começar sem abrir mão da autenticidade e dos valores do ministério."
keywords: "pastor presenca digital, igreja digital, lider religioso internet, ministerio online, como comecar presenca digital pastor"
author: "Equipe LuzLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/presenca-digital-pastor.jpg"
culture: "pt-BR"
category: "ministerio"
---
# Por Que Pastores Precisam de Presença Digital
Há um argumento que muitos líderes religiosos usam para resistir ao mundo digital: "Meu trabalho é com pessoas, não com telas."
É um argumento honesto. E parcialmente correto — o coração do ministério é mesmo o encontro real, a comunhão presencial, o olho no olho.
Mas esse argumento ignora uma realidade importante: as pessoas que você quer alcançar estão online. E se você não está lá, alguém com uma mensagem completamente diferente está.
---
## Este artigo é para você se...
- Você ainda não tem presença digital — ou tem algo desatualizado que não representa bem o seu ministério
- Você já ouviu falar em "estar online" mas não está convencido de que faz diferença para um ministério
- Você quer entender os riscos reais de não estar no digital, com exemplos práticos
- Você quer começar de forma simples, sem transformar a vida em produção de conteúdo
**O que você encontra aqui:** por que a presença digital mudou de opcional para necessária, os custos reais de não estar online, e o mínimo que já faz diferença — com passos concretos para começar.
---
## Como as pessoas encontram novas igrejas hoje
Vinte anos atrás, uma família que se mudava para uma cidade nova perguntava para vizinhos ou parentes onde havia uma boa igreja. Hoje, ela abre o Google e digita "igreja evangélica perto de mim" ou "pastor presbitariano em [cidade]".
Se a sua congregação não aparece nessa busca, simplesmente não existe para essa família.
Pesquisas no Brasil mostram que mais de 70% das pessoas que começaram a frequentar uma nova comunidade religiosa nos últimos anos a descobriram primeiro online — seja pelo YouTube, pelo Instagram ou por uma pesquisa no Google.
A janela de entrada da sua congregação não é mais apenas a porta física da sede. É também o que aparece quando alguém pesquisa sobre seu ministério na internet.
---
## A presença digital não substitui o presencial
Antes de continuar, é importante deixar isso claro: presença digital não é concorrente do ministério presencial. É complemento.
Um culto ao vivo transmitido pelo YouTube não substitui a comunhão de estar presente no templo. Uma pregação no Instagram não tem a profundidade de um retiro espiritual.
Mas a live pode alcançar um membro que está doente e não pode sair. A pregação no Instagram pode ser o primeiro contato de alguém que nunca pisou numa igreja mas está buscando algo diferente.
O ministério digital abre portas. O ministério presencial é o que acontece quando as pessoas entram por essas portas.
---
## O custo de não estar online
Quando um ministério não tem presença digital, o custo não é apenas invisibilidade. São oportunidades concretas que se perdem:
**Pessoas que estão buscando mas não encontram**
Alguém com dúvidas espirituais pesquisa no YouTube por respostas. Encontra vários canais de líderes religiosos. O seu não está lá.
**Membros que se afastam por falta de conexão**
Um membro que se muda para outra cidade ou que passa por um período de dificuldade para comparecer presencialmente perde o vínculo com a comunidade se não há uma forma de continuar conectado.
**Jovens que não se identificam com o que veem**
Para a geração que cresceu com smartphone, uma comunidade que não tem presença digital parece desatualizada — mesmo que o conteúdo da mensagem seja profundo e relevante.
**Crises sem canais de comunicação**
Em situações de urgência — cancelamento de evento, mudança de horário, situação de emergência — um ministério sem canais digitais depende de ligações individuais ou grupos de WhatsApp desorganizados.
---
## O mínimo que faz diferença
Você não precisa ter uma equipe de produção de conteúdo ou postar todos os dias. O mínimo que já muda a situação de um ministério:
### 1. Um ponto de referência digital
Uma página simples com: nome do ministério, endereço, horários de culto, contato e links para redes sociais. Isso já garante que alguém que pesquisa seu nome encontre algo.
O LuzLinks foi criado exatamente para isso — criar esse ponto de referência de forma simples, sem precisar montar um site completo.
### 2. Presença consistente em uma plataforma
Escolha uma plataforma e mantenha-a atualizada regularmente. YouTube funciona bem para ministérios com pregações. Instagram funciona bem para comunidade e rotina. WhatsApp funciona bem para comunicação interna.
Você não precisa estar em todos os lugares. Mas precisa estar em algum lugar de forma consistente.
### 3. Comunicação clara sobre eventos
As pessoas precisam saber quando e onde os cultos acontecem. Isso parece óbvio, mas é surpreendente quantos ministérios não têm essa informação em lugar nenhum online.
---
## Objeções comuns — e respostas honestas
**"Não tenho tempo para ficar postando."**
Presença digital não precisa ser conteúdo novo todos os dias. Uma pregação postada por mês no YouTube, um post de agenda no Instagram por semana e um grupo de WhatsApp organizado já são suficientes para começar.
**"Tenho medo de expor minha vida pessoal."**
Você define o que compartilha. Muitos líderes mantêm uma presença digital completamente focada no ministério — pregações, estudos, agenda — sem expor nada da vida privada. Isso é possível e saudável.
**"Minha congregação é mais velha e não usa redes sociais."**
Parte da sua congregação atual pode não usar. Mas as pessoas que você ainda não alcançou — os filhos desses membros, a família de quem já frequenta — usam. E quando esses membros mais velhos indicam sua igreja para alguém, a primeira coisa que essa pessoa vai fazer é pesquisar online.
**"Não sei nada de tecnologia."**
Esse argumento ficou mais fraco com o tempo. As ferramentas de hoje são projetadas para serem usadas por qualquer pessoa. Criar uma página no LuzLinks, por exemplo, leva menos de 30 minutos e não exige nenhum conhecimento técnico.
---
## Por onde começar
Se você ainda não tem nenhuma presença digital, comece pelos fundamentos:
**Passo 1:** Crie uma conta no LuzLinks e monte uma página simples com os links do seu ministério. Isso leva menos de meia hora.
**Passo 2:** Coloque esse link na bio do Instagram e no status do WhatsApp.
**Passo 3:** Se você já grava os cultos em vídeo, comece a postar no YouTube. Mesmo sem edição — uma gravação direta da câmera do celular já serve para começar.
**Passo 4:** Mantenha as informações atualizadas. Horários, endereço, links — nada de informação desatualizada.
Depois que esses fundamentos estiverem funcionando, você avança com mais tranquilidade para outras plataformas, se quiser.
---
## A pergunta certa
A pergunta não é "por que devo estar online?" — porque as razões são claras.
A pergunta é: "Como posso estar online de uma forma que seja autêntica ao meu ministério e sustentável para a minha rotina?"
E essa é uma pergunta que você pode responder com calma, no seu ritmo, começando pelo mais simples.
---
## O que pode não funcionar
- **Tentar estar em todas as plataformas ao mesmo tempo:** leva à superficialidade em todas. Comece com uma e domine antes de expandir
- **Postar esporadicamente:** o algoritmo e a audiência valorizam consistência. Uma postagem por semana, sempre, supera dez postagens num mês e sumindo depois
- **Separar completamente digital do presencial:** a presença digital deve convidar para o encontro real, não substituí-lo. Ministérios que tratam os dois como mundos separados perdem o sentido de comunidade
- **Não monitorar o que está funcionando:** criar conteúdo sem nunca verificar se as pessoas estão chegando é trabalho sem aprendizado
---
## Leia também
- [Como Criar Sua Bio de Fé do Zero](/artigos/como-criar-sua-bio-de-fe-do-zero)
- [Como Divulgar Cultos e Eventos pelo WhatsApp com um Único Link](/artigos/como-divulgar-cultos-e-eventos-pelo-whatsapp)
- [Como Receber Dízimos Online no Seu Ministério](/artigos/como-receber-dizimos-online-no-seu-ministerio)
---
**Pronto para dar o primeiro passo?**
Crie a página do seu ministério no LuzLinks e garanta que as pessoas que estão buscando uma comunidade de fé possam te encontrar.
[Criar minha página no LuzLinks →](https://luzlinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -1,197 +0,0 @@
---
title: "Como Configurar Sua Bio para Ganhar Mais Seguidores"
description: "Sua bio é a porta de entrada do seu perfil. Veja como configurar uma página de links que converte visitantes em seguidores — e seguidores em assinantes."
keywords: "bio link seguidores, como ganhar seguidores instagram, bio link criadora, aumentar seguidores bio, configurar bio profissional"
author: "Equipe SpicyLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/bio-seguidores.jpg"
culture: "pt-BR"
category: "criadores"
---
# Como Configurar Sua Bio para Ganhar Mais Seguidores
A bio do Instagram tem um limite de 150 caracteres e permite um único link. Com esse espaço pequeno, você precisa convencer uma pessoa que acabou de ver seu conteúdo pela primeira vez a clicar, explorar e — idealmente — te seguir ou assinar.
A maioria das criadoras desperdiça essa oportunidade. Este artigo mostra como não desperdiçar.
---
## Este artigo é para você se...
- Você já cria conteúdo no Instagram mas sente que poderia estar convertendo mais visitantes em seguidores
- Você não sabe exatamente o que colocar na bio ou na página de links para chamar atenção
- Você quer uma estratégia clara — não dicas genéricas, mas um passo a passo que dá para aplicar hoje
- Você está crescendo e quer que a estrutura da sua bio acompanhe esse crescimento
**O que você encontra aqui:** como a jornada do visitante funciona, o que uma boa foto e bio fazem, como organizar os links por prioridade, e como usar métricas para melhorar ao longo do tempo.
---
## A jornada do visitante
Antes de falar sobre configuração, entenda o caminho que uma pessoa percorre antes de clicar em "seguir":
1. **Descobre você** — pelo feed, por um Reels, por um compartilhamento, por indicação
2. **Visita seu perfil** — analisa a foto, lê a bio, olha os posts recentes
3. **Clica no link da bio** — se a bio despertou interesse suficiente
4. **Explora a página de links** — vê o que você oferece além do que já viu
5. **Toma uma decisão** — seguir, assinar, ou sair
Cada etapa é uma chance de perder ou converter o visitante. Sua bio e sua página de links juntas determinam o que acontece nas etapas 2 a 4.
---
## A foto de perfil: primeiro impacto visual
Antes de ler uma palavra da sua bio, a pessoa vê sua foto. Ela precisa passar algumas mensagens em frações de segundo:
**Identificação clara:** É fácil reconhecer quem é você? O rosto deve estar visível e nítido, mesmo na versão pequena (a miniatura do Instagram é pequena).
**Coerência com o conteúdo:** Se você produz conteúdo com uma estética específica, a foto de perfil deve refletir isso. Quem clica no seu perfil depois de ver um Reels quer reconhecer a mesma pessoa.
**Qualidade:** Não precisa ser fotógrafo profissional, mas precisa ter boa iluminação. Uma janela com luz natural já faz grande diferença.
**Dica prática:** Evite fotos de grupo, fotos muito escuras, selfies com câmera frontal em ambientes com pouca luz, ou fotos com filtro tão forte que desfiguram o rosto.
---
## Os 150 caracteres da bio: cada palavra conta
A bio do Instagram tem espaço para aproximadamente 150 caracteres — umas 2 ou 3 linhas curtas. Esse é o seu espaço para responder a pergunta que o visitante está fazendo mentalmente: *"O que vou encontrar aqui?"*
**O que funciona na bio:**
**Uma descrição direta do conteúdo**
> "Conteúdo exclusivo toda semana 🔥 | Fitness e estilo de vida | Link abaixo 👇"
**Uma proposta de valor com emoji**
> "Sua dose diária de entretenimento ✨ | Novidades toda semana | Acesse meu link 👇"
**Uma chamada para ação com curiosidade**
> "Tem conteúdo exclusivo que não posto aqui 👀 | Veja no meu link ↓"
**O que evitar:**
❌ Bio vazia ou só com o nome
❌ Lista de redes sociais que já estão visíveis no perfil
❌ Frases muito genéricas que poderiam ser de qualquer pessoa
❌ Texto longo demais que a pessoa não lê
---
## A página de links: onde a conversão acontece
Quando alguém clica no link da sua bio, chega na sua página do SpicyLinks. É aqui que você tem mais espaço para convencer.
**Capriche no nome e na descrição da página**
O nome é o que aparece em destaque. Use como você quer ser reconhecida — seu nome artístico, apelido, ou o nome que você usa nas redes.
A descrição da página é mais longa que a bio do Instagram. Use para dar contexto sobre quem você é e o que oferece. Seja específica:
*Evite:* "Criadora de conteúdo"
*Prefira:* "Criadora de conteúdo de lifestyle e fitness. Conteúdo exclusivo toda semana, dicas de treino e meus looks favoritos."
**A ordem dos links define o que converte**
As primeiras posições recebem a maioria dos cliques. Coloque no topo o que você mais quer que as pessoas acessem:
```
1. Conteúdo exclusivo / plataforma principal
2. Instagram (para quem ainda não te segue lá)
3. Twitter/X (se você usa ativamente)
4. TikTok (se você usa)
5. Lista de desejos / outras plataformas
```
Se o objetivo é ganhar mais seguidores nas redes gratuitas, coloque os links delas antes das plataformas pagas. Se o objetivo é converter em assinantes, coloque a plataforma paga primeiro.
**O título de cada link importa**
O título do link é o que a pessoa lê antes de clicar. "Instagram" como título é genérico. Mas "Me segue lá também 📸" ou "Meus stories do dia a dia" já cria uma razão para clicar.
Teste títulos diferentes e veja o que gera mais cliques nas suas métricas.
---
## Consistência visual entre perfil e página de links
Um detalhe que faz diferença: a aparência da sua página de links deve combinar com a estética do seu perfil.
Se você tem um feed com paleta de cores clara e minimalista, escolha um tema do SpicyLinks que reflita isso. Se seu perfil é vibrante e colorido, vá em direção a algo mais ousado.
Quando a pessoa passa do Instagram para a sua página de links e vê uma identidade visual coerente, a sensação é de profissionalismo. Quando parece uma coisa diferente, causa estranheza — e estranheza não converte.
---
## Usando o SpicyLinks para crescer no Instagram especificamente
Se crescer no Instagram é o objetivo prioritário, configure sua página assim:
**Posição 1:** Um link para o seu Instagram com título que incentive o follow
**Posição 2:** Seu conteúdo principal (plataforma exclusiva, YouTube, etc.)
**Posição 3+:** Outras redes e links relevantes
**Na bio do Instagram**, teste este tipo de texto:
> "Sempre tem coisa nova aqui 📲 | Conteúdo exclusivo no link 👇"
Ao invés de só falar em "conteúdo exclusivo pago", mencione o que é gratuito também. A pessoa que ainda não está pronta para pagar pode te seguir agora e assinar mais tarde.
---
## Análise: o que está funcionando?
Configure sua página e depois use as métricas disponíveis para entender o comportamento das pessoas:
- Qual link recebe mais cliques?
- Quantas pessoas visitam a página mas não clicam em nada?
- De onde as pessoas chegam na sua página (Instagram, Twitter, etc.)?
Se muita gente visita mas poucos clicam, o problema pode estar nos títulos dos links ou na descrição da página.
Se muita gente clica no Instagram mas poucos clicam no conteúdo exclusivo, talvez valha a pena testar uma ordem diferente dos links ou um título mais atrativo para a plataforma principal.
---
## Checklist para uma bio que converte
Antes de publicar, confira:
- [ ] Foto de perfil nítida e com rosto visível
- [ ] Bio do Instagram clara, com no máximo 3 linhas
- [ ] Link da bio atualizado (apontando para o SpicyLinks)
- [ ] Página do SpicyLinks com nome e descrição preenchidos
- [ ] Links organizados por prioridade
- [ ] Títulos dos links descritivos (não só o nome da plataforma)
- [ ] Tema visual coerente com a estética do perfil
- [ ] Todos os links testados (clicando para confirmar que funcionam)
---
## O que pode não funcionar
- **Foto de perfil genérica ou de baixa qualidade:** é a primeira coisa que as pessoas veem — uma foto ruim faz a pessoa sair antes de ler a bio
- **Bio do Instagram e página de links com visual completamente diferente:** a falta de coerência visual causa estranheza e diminui a confiança
- **Atualizar a bio raramente:** o que convertia bem 6 meses atrás pode não converter mais — revise os títulos dos links e a descrição a cada 2 ou 3 meses
- **Muitos links sem hierarquia:** 10 links todos com a mesma importância visual fazem a pessoa não saber o que clicar; prioridade clara é fundamental
---
## Leia também
- [Os Melhores Links para Colocar na Sua Bio do Instagram](/artigos/os-melhores-links-para-sua-bio-do-instagram)
- [Como Criadores de Conteúdo Ganham Dinheiro com Links de Afiliados](/artigos/como-criadores-de-conteudo-ganham-dinheiro-com-afiliados)
- [SpicyLinks vs Linktree: Qual é Melhor para Criadores?](/artigos/spicylinks-vs-linktree-qual-e-melhor-para-criadores)
---
**Sua bio pronta para converter visitantes em seguidores?**
Configure sua página no SpicyLinks e transforme cada visita em uma oportunidade.
[Criar meu SpicyLinks →](https://spicylinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -1,204 +0,0 @@
---
title: "Como Criadores de Conteúdo Ganham Dinheiro com Links de Afiliados"
description: "Marketing de afiliados é uma das formas mais acessíveis de renda extra para criadores. Veja como funciona, como começar e como usar sua bio de links para maximizar as conversões."
keywords: "links afiliados criadora, marketing afiliados instagram, ganhar dinheiro afiliados, renda extra criadora conteudo, afiliados bio link"
author: "Equipe SpicyLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/afiliados-criadores.jpg"
culture: "pt-BR"
category: "criadores"
---
# Como Criadores de Conteúdo Ganham Dinheiro com Links de Afiliados
Você recomenda roupas, produtos de beleza, acessórios, equipamentos de fotografia — coisas que realmente usa e aprecia. Seus seguidores confiam no que você indica. Mas quando alguém vai comprar, você não vê nada dessa venda.
O marketing de afiliados muda isso: você ganha uma comissão cada vez que alguém compra um produto pela sua indicação. E a parte boa é que não precisa estocar nada, não precisa vender diretamente, e o produto nem precisa ser seu.
Este artigo explica como funciona e como você pode começar.
---
## Este artigo é para você se...
- Você recomenda produtos para suas seguidoras mas ainda não monetiza essas indicações
- Você quer uma fonte de renda que funcione mesmo quando não está postando
- Você quer entender o básico de afiliados sem enrolação — como funciona, por onde começa e quanto dá para ganhar de forma realista
- Você quer saber como organizar os links de afiliado na sua bio de forma profissional
**O que você encontra aqui:** como funciona o marketing de afiliados, os principais programas disponíveis no Brasil, como divulgar sem parecer forçada e como organizar tudo na sua bio do SpicyLinks.
---
## O que é marketing de afiliados
Marketing de afiliados é um modelo onde uma empresa te paga uma comissão pela venda que você gerou. Você recebe um link único (seu link de afiliado), compartilha com sua audiência, e quando alguém compra através desse link, você recebe uma porcentagem do valor.
**Exemplo prático:**
Você usa um conjunto de maquiagem que adora. A marca tem um programa de afiliados e te oferece 15% de comissão. Você posta sobre o produto com seu link de afiliado. Uma seguidora compra — digamos R$ 200. Você recebe R$ 30 dessa venda, sem ter feito mais nada além de compartilhar.
Multiplique isso por vários produtos, várias seguidoras e um período de semanas ou meses, e fica claro por que afiliados podem ser uma fonte relevante de renda.
---
## Por que afiliados funcionam bem para criadores de conteúdo
**Sua audiência já confia em você**
A principal vantagem de um criador de conteúdo no marketing de afiliados é a relação de confiança já construída. Uma recomendação sua tem muito mais peso do que um anúncio tradicional. Suas seguidoras te conhecem, te acompanham, e tendem a confiar no que você indica.
**Não exige capital inicial**
Diferente de vender produtos próprios, afiliados não exigem nenhum investimento. Você não compra estoque, não precisa de depósito, não tem risco financeiro.
**Funciona enquanto você dorme**
Um link de afiliado colocado na sua bio continua gerando comissões 24 horas por dia, mesmo quando você não está postando. Um post antigo com link de afiliado pode gerar comissão meses depois que foi publicado.
**Complementa outras formas de receita**
Afiliados não concorrem com suas assinaturas ou com outras fontes de renda — eles somam.
---
## Os principais programas de afiliados no Brasil
### Amazon Associates (Parceiros Amazon)
O maior programa de afiliados do mundo. Qualquer produto vendido na Amazon pode ser divulgado com comissão — de 2% a 10% dependendo da categoria.
**Ideal para:** maquiagem, acessórios, eletrônicos, roupas, decoração, livros, qualquer coisa que você compra e usa.
Como entrar: [associados.amazon.com.br](https://associados.amazon.com.br) — cadastro gratuito, aprovação em poucos dias.
### Hotmart, Eduzz e Monetizze
Plataformas brasileiras de produtos digitais (cursos, ebooks, programas). Comissões costumam ser mais altas — 30% a 60% do valor do produto.
**Ideal para:** indicar cursos relacionados ao seu nicho (fotografia, edição de vídeo, fitness, etc.)
### Shein, Renner, Dafiti (programas próprios)
Lojas de moda que têm programas de afiliados diretos. Comissões menores (3-8%), mas produtos com alta taxa de compra entre seguidoras de moda e lifestyle.
### Programa de Afiliados do SpicyLinks
O Plano Premium+Afiliados do SpicyLinks permite incluir links de afiliado com tracking diretamente na sua página, de forma organizada e com métricas de cliques para cada link.
---
## O que você precisa para começar
**1. Escolha um ou dois programas**
Não tente entrar em todos de uma vez. Comece com o que faz mais sentido para o seu nicho e o que você já compra naturalmente.
**2. Crie sua conta nos programas escolhidos**
A maioria exige apenas email, CPF e dados bancários para pagamento. Aprovação costuma ser rápida para quem já tem alguma audiência.
**3. Gere seus links de afiliado**
Cada produto que você quiser divulgar tem um link único com seu código. Quando alguém compra por esse link, a venda é atribuída a você.
**4. Compartilhe de forma autêntica**
Divulgue produtos que você realmente usa ou que fazem sentido para o seu público. Seguidoras percebem quando a indicação é genuína — e quando não é.
---
## Como organizar os links de afiliado na sua bio
Uma das melhores formas de aproveitar links de afiliado é mantê-los sempre visíveis na sua página do SpicyLinks.
**Estrutura sugerida:**
```
🛒 Favoritos que uso (lista de afiliados)
→ Cuidados com a pele que eu amo
→ Meus itens de roupa favoritos do mês
→ Equipamentos que uso para produzir
```
Você pode criar uma seção específica de "produtos que recomendo" na sua página, com links de afiliado para cada categoria.
**Vantagem do SpicyLinks:** Com o plano Premium+Afiliados, você pode incluir links de produto com tracking, vendo exatamente quantas pessoas clicam em cada recomendação.
---
## Como divulgar sem parecer forçada
Este é o ponto que diferencia criadoras que conseguem renda com afiliados das que irritam a audiência tentando vender o tempo todo.
**Regras práticas:**
**Só indique o que você genuinamente usa ou aprovaria usar**
Se você nunca testou o produto, não é indicação — é anúncio. Sua audiência sente a diferença, e um produto ruim indicado por você vai custar credibilidade.
**Seja transparente sobre a parceria**
Quando você usa um link de afiliado, é boa prática mencionar isso. Algo simples como "link de afiliado — não custa nada extra pra você, mas me ajuda" é suficiente. Além de ser honesta, essa transparência costuma aumentar — não diminuir — a conversão.
**Contextualize a indicação**
"Esse protetor solar é do meu link de afiliados" converte menos do que "Uso esse protetor todo dia e nunca descamou no meu rosto — link pra comprar abaixo". O contexto cria o desejo.
**Não exagere na frequência**
Se todo post é uma indicação de produto, sua audiência passa a ignorar. Reserve as indicações de afiliados para momentos em que surgem naturalmente — quando você realmente está usando o produto ou quando alguém pergunta.
---
## Quanto dá para ganhar?
A resposta honesta é: depende muito da audiência e de como você usa.
Uma criadora com 10 mil seguidores engajados pode ganhar mais com afiliados do que outra com 100 mil seguidores que não engaja.
Para ter uma referência:
- Um link de afiliado para produto de R$ 150 com 5% de comissão = R$ 7,50 por venda
- Se 20 seguidoras compram num mês = R$ 150
- Com 5 produtos diferentes gerando 20 vendas cada = R$ 750 por mês em comissões
Isso é adicional à renda de assinaturas e publipost, sem trabalho extra além de compartilhar links que você já usaria para indicar os produtos mesmo sem ser paga.
Com o tempo e com o crescimento da audiência, esses números escalam.
---
## Erros para evitar
**Indicar produtos de má qualidade por comissão mais alta**
A comissão não compensa a perda de credibilidade com a audiência.
**Usar links de afiliado em stories sem salvar nos destaques**
Stories somem em 24 horas. Mantenha os links de afiliado sempre disponíveis na sua bio do SpicyLinks.
**Não acompanhar o que está convertendo**
Use as métricas disponíveis nos programas de afiliados e na sua página do SpicyLinks para saber quais produtos geram mais cliques e vendas.
**Esquecer de atualizar quando um produto muda ou sai de linha**
Links quebrados geram frustração e custam credibilidade.
---
## O que pode não funcionar
- **Indicar produtos sem ter testado:** uma indicação de produto ruim custa muito mais do que a comissão ganha — a credibilidade com a audiência é seu ativo mais valioso
- **Não deixar os links de afiliado sempre visíveis:** guardar para posts esporádicos reduz muito a conversão; links fixos na bio trabalham 24h por dia
- **Esperar resultados rápidos:** afiliados são renda que cresce com o tempo e com o crescimento da audiência — nos primeiros meses os valores são pequenos, mas escalam
- **Não declarar que é link de afiliado:** além de ser a prática honesta, a transparência aumenta a confiança e frequentemente melhora a conversão
---
## Leia também
- [Os Melhores Links para Colocar na Sua Bio do Instagram](/artigos/os-melhores-links-para-sua-bio-do-instagram)
- [Como Configurar Sua Bio para Ganhar Mais Seguidores](/artigos/como-configurar-sua-bio-para-ganhar-mais-seguidores)
- [SpicyLinks vs Linktree: Qual é Melhor para Criadores?](/artigos/spicylinks-vs-linktree-qual-e-melhor-para-criadores)
---
**Pronta para adicionar afiliados à sua estratégia de renda?**
Com o plano Premium+Afiliados do SpicyLinks, você organiza e acompanha todos os seus links de produto em um só lugar.
[Criar meu SpicyLinks →](https://spicylinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -1,212 +0,0 @@
---
title: "Os Melhores Links para Colocar na Sua Bio do Instagram"
description: "Quais links realmente valem a pena ter na sua bio? Uma análise prática sobre o que coloca, o que evita e como organizar para converter mais — seja em seguidores, assinantes ou vendas."
keywords: "links bio instagram, o que colocar na bio, melhores links bio criadora, bio link instagram criador, links que convertem instagram"
author: "Equipe SpicyLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/links-bio-instagram.jpg"
culture: "pt-BR"
category: "criadores"
---
# Os Melhores Links para Colocar na Sua Bio do Instagram
O Instagram permite um único link na bio. Então a maioria das criadoras coloca uma página com vários links — e essa decisão, quando bem feita, multiplica as possibilidades de conversão.
Mas quais links colocar? Em que ordem? O que realmente faz diferença?
Este artigo responde essas perguntas com base no que funciona na prática.
---
## Este artigo é para você se...
- Você tem uma página de links mas não sabe se está aproveitando bem o espaço
- Você adiciona links sem critério e quer entender qual ordem gera mais resultados
- Você quer saber o que realmente converte — e o que está desperdiçando o espaço dos primeiros cliques
- Você acabou de criar sua conta e quer montar a bio já com uma estrutura inteligente
**O que você encontra aqui:** a regra dos três primeiros links, quais links valem a pena incluir por objetivo, o que evitar, modelos prontos para diferentes estratégias e como manter a página atualizada.
---
## A regra dos primeiros três links
Pesquisas de comportamento em páginas de links mostram um padrão consistente: os três primeiros links recebem entre 60% e 80% de todos os cliques. O quarto em diante recebe cada vez menos atenção.
Isso significa que a decisão mais importante na sua bio de links não é quantos links ter, mas *quais links ficam nas três primeiras posições*.
Defina seu objetivo antes de ordenar:
- Se o objetivo é assinantes: plataforma exclusiva no topo
- Se o objetivo é crescer no Instagram: Instagram no topo (para quem chega por outra rede)
- Se o objetivo é vendas: loja ou lista de desejos no topo
---
## Os links que mais valem a pena
### 1. Sua plataforma de conteúdo exclusivo
Se você tem uma plataforma de assinatura, este deve ser o primeiro link. É a sua principal fonte de receita e merece a posição de maior visibilidade.
**Como nomear:** Não coloque só o nome da plataforma. Crie um título que comunique valor:
✅ "Conteúdo exclusivo que não posto em lugar nenhum 🔥"
✅ "Assinar — novidade toda semana ❤️"
✅ "Acesso VIP ao meu conteúdo especial"
❌ "OnlyFans"
❌ "Minha plataforma"
❌ "Link aqui"
### 2. Instagram
Parece estranho colocar o link do Instagram na bio do próprio Instagram, mas faz sentido em dois casos:
**Caso 1:** Você compartilha sua bio em outros lugares (WhatsApp, Twitter/X, TikTok). Quem chega por essas plataformas pode não te seguir no Instagram ainda.
**Caso 2:** Você tem um perfil secundário ou um perfil profissional separado que quer divulgar.
Se você só tem um Instagram e compartilha a bio principalmente por lá, esse link pode ir para posições mais baixas ou nem aparecer.
### 3. Twitter/X ou TikTok
Se você tem presença ativa nessas plataformas e quer crescer lá também, inclua. Escolha uma ou duas — não tente colocar todas as redes.
**Dica:** Coloque a plataforma onde seu conteúdo mais converte ou onde você tem mais potencial de crescimento.
### 4. Lista de desejos
Uma lista de desejos (Amazon Wishlist ou similar) é receita passiva. Fãs que querem presentear encontram aqui o que você quer. Quanto mais visível, mais presente funciona.
**Como nomear:**
✅ "Minha lista de desejos 🎁"
✅ "Me presentear? Tem aqui 🥰"
### 5. WhatsApp ou Telegram VIP
Se você tem um grupo privado de fãs mais próximos, coloque o link de acesso. Funciona bem como um nível intermediário — entre seguir gratuitamente e assinar o conteúdo exclusivo.
### 6. YouTube
Se você posta conteúdo gratuito no YouTube (vlogs, tutoriais, bastidores), inclua o canal. Conteúdo gratuito de qualidade constrói confiança e eventualmente converte em assinante.
### 7. Email para parcerias
Se você quer fechar contratos com marcas, um email profissional dedicado a parcerias é fundamental. Coloque com um título claro:
✅ "Parcerias e publipost → email aqui"
✅ "Contato comercial"
---
## Links que você provavelmente não precisa incluir
**Linktree ou outras ferramentas de bio link além do SpicyLinks**
Não faz sentido ter um link para outra página de links dentro da sua página de links. Centralize tudo em um lugar só.
**Facebook**
A menos que você tenha uma página ativa e relevante no Facebook, não precisa incluir. A plataforma tem relevância muito menor para o público jovem que consome conteúdo de criadores.
**Snapchat**
Plataforma com uso muito específico. Inclua apenas se for uma parte real da sua estratégia.
**Link para post específico**
Links para posts individuais ficam desatualizados rápido. Prefira linkar para o seu perfil ou canal — assim o visitante sempre vê o conteúdo mais recente.
---
## A questão da quantidade: menos é mais?
Não necessariamente. A pergunta certa não é "quantos links", mas "cada link aqui tem um propósito claro?"
Uma página com 4 links bem escolhidos converte melhor do que uma com 12 links desorganizados onde a pessoa não sabe o que clicar.
**Regra prática:** Se você hesita se um link deve estar na página, provavelmente não precisa estar.
---
## Organização por objetivo: modelos prontos
**Para quem quer maximizar assinaturas:**
```
1. Conteúdo exclusivo (plataforma principal)
2. Segundo perfil (plataforma alternativa)
3. Lista de desejos
4. Instagram
5. Twitter/X
6. Email para parcerias
```
**Para quem quer crescer em seguidores:**
```
1. Instagram
2. TikTok
3. Twitter/X
4. Conteúdo exclusivo
5. Lista de desejos
6. Email para parcerias
```
**Para quem quer equilibrar crescimento e receita:**
```
1. Conteúdo exclusivo
2. Instagram
3. TikTok ou YouTube
4. Lista de desejos
5. Email para parcerias
```
---
## Mantendo a página atualizada
Sua página de links precisa de manutenção periódica. A cada 2-4 semanas, verifique:
- Todos os links ainda funcionam?
- Os títulos continuam relevantes?
- Surgiu alguma nova plataforma ou oferta que merece aparecer?
- Algum link está desatualizado e deveria ser removido?
Uma página com links quebrados ou informações antigas passa uma impressão de descuido — o contrário do que você quer para a sua imagem profissional.
---
## Testando o que funciona
Se você usa o SpicyLinks, as métricas mostram quantas pessoas clicam em cada link. Use esses dados para tomar decisões:
**Se um link tem muitas visitas mas poucos cliques:** O título pode não estar sendo atraente o suficiente. Teste uma versão diferente por 2 semanas e compare.
**Se os links de baixo nunca são clicados:** Eles estão ocupando espaço de links que poderiam converter mais. Considere remover ou reorganizar.
**Se você recebe muitas visitas mas conversão baixa:** O problema pode ser na descrição da página ou na coerência entre o que as pessoas esperam e o que encontram.
---
## O que pode não funcionar
- **Título genérico nos links:** "Instagram" ou "OnlyFans" não convence ninguém — o título precisa comunicar o que a pessoa vai encontrar ao clicar
- **Links que apontam para conteúdo específico desatualizado:** um post antigo ou uma live encerrada gera frustração; prefira links para perfis ou canais completos
- **Não testar variações:** a ordem e o título dos links que você configurou hoje não são definitivos — teste, analise e ajuste
- **Ignorar as métricas:** se um link nunca recebe cliques, ele está ocupando o espaço de algo que poderia converter
---
## Leia também
- [Como Configurar Sua Bio para Ganhar Mais Seguidores](/artigos/como-configurar-sua-bio-para-ganhar-mais-seguidores)
- [SpicyLinks vs Linktree: Qual é Melhor para Criadores?](/artigos/spicylinks-vs-linktree-qual-e-melhor-para-criadores)
- [Como Criadores de Conteúdo Ganham Dinheiro com Links de Afiliados](/artigos/como-criadores-de-conteudo-ganham-dinheiro-com-afiliados)
---
**Configure sua bio com os links certos e veja a diferença.**
Crie ou atualize sua página no SpicyLinks com uma estratégia clara.
[Criar meu SpicyLinks →](https://spicylinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -1,210 +0,0 @@
---
title: "SpicyLinks vs Linktree: Qual é Melhor para Criadores?"
description: "Uma comparação direta entre SpicyLinks e Linktree para criadores de conteúdo brasileiros — preço, recursos, suporte e o que cada plataforma realmente entrega."
keywords: "spicylinks vs linktree, linktree alternativa brasil, melhor bio link criadora, linktree concorrente, bio link para criadores"
author: "Equipe SpicyLinks"
date: 2026-04-30
lastMod: 2026-04-30
image: "/images/artigos/spicylinks-vs-linktree.jpg"
culture: "pt-BR"
category: "criadores"
---
# SpicyLinks vs Linktree: Qual é Melhor para Criadores?
O Linktree é a plataforma de bio link mais conhecida do mundo. Então por que existem alternativas — e por que algumas criadoras preferem usar o SpicyLinks?
Este artigo faz uma comparação direta, sem rodeios, para ajudar você a decidir o que faz mais sentido para a sua situação.
---
## Este artigo é para você se...
- Você está avaliando qual plataforma de bio link usar e não sabe a diferença entre as opções
- Você já usa o Linktree mas está questionando se vale a pena continuar — especialmente pelo custo em dólar
- Você quer uma comparação direta, sem papo de vendedor, sobre o que cada plataforma entrega
- Você quer saber se migrar faz sentido e se o processo é complicado
**O que você encontra aqui:** pontos fortes e fracos de cada plataforma, comparação de preços real (dólar vs real), diferenças nos recursos principais e quando cada uma faz sentido.
---
## O Linktree: o que é e para quem serve bem
O Linktree foi lançado em 2016 e hoje tem mais de 40 milhões de usuários. É a escolha padrão para quem quer algo rápido, reconhecível e sem muita configuração.
**Pontos fortes do Linktree:**
- Reconhecimento de marca — todo mundo sabe o que é
- Versão gratuita disponível
- Interface simples, fácil de aprender
- Integrações com várias plataformas
- Disponível em muitos idiomas
**Limitações do Linktree:**
- Preços em dólar (caro para o bolso brasileiro)
- Suporte em inglês
- Não tem moderação de conteúdo — qualquer conteúdo pode ser hospedado
- Termos de serviço genéricos, não adaptados ao criador brasileiro
- Analytics básico nos planos gratuito e mais barato
- URL padrão é `linktr.ee/seu-nome` — não tem URL personalizada com contexto
---
## O SpicyLinks: para quem foi feito
O SpicyLinks foi criado especificamente para criadores de conteúdo brasileiros que precisam de uma plataforma que entenda a realidade local — em termos de preços, moeda, suporte e tipos de conteúdo.
**Diferenciais principais:**
- Preços em reais, sem variação cambial
- URL com contexto: `spicylinks.site/categoria/seu-nome`
- Suporte em português
- Plano com links de afiliados nativos
- Analytics de cliques por link
- Moderação que protege a plataforma e os criadores
- Temas visuais adaptados para o nicho de criadores
---
## Comparação direta
### Preço
| Plano | Linktree | SpicyLinks |
|-------|----------|-----------|
| Gratuito/Trial | Sim (limitado) | 7 dias grátis |
| Plano básico | US$ 5/mês (~R$ 28) | R$ 12,90/mês |
| Plano intermediário | US$ 9/mês (~R$ 51) | R$ 25,90/mês |
| Plano completo | US$ 24/mês (~R$ 136) | R$ 29,90/mês |
| Com afiliados | Não incluso | R$ 34,90/mês |
*Cotação do dólar varia. Preços Linktree podem ter se alterado desde esta publicação.*
**Vantagem:** SpicyLinks em todos os planos — com preço significativamente menor em reais e sem risco de variação cambial.
---
### URL personalizada
**Linktree:** Sua URL fica `linktr.ee/seu-nome`. Não tem contexto — qualquer pessoa pode ter essa URL, de qualquer nicho.
**SpicyLinks:** Sua URL inclui sua categoria: `spicylinks.site/modelos/seu-nome` ou `spicylinks.site/influencers/seu-nome`. Isso cria contexto e credibilidade — a URL já comunica quem você é antes de a pessoa clicar.
**Vantagem:** SpicyLinks para quem quer uma presença mais profissional e contextualizada.
---
### Links de afiliados
**Linktree:** Você pode colocar links de afiliados manualmente como qualquer outro link, mas não tem funcionalidade específica para isso — sem tracking por link dentro da plataforma.
**SpicyLinks:** O Plano Premium+Afiliados inclui links de produto com tracking nativo. Você vê exatamente quantas pessoas clicam em cada link de afiliado, o que permite otimizar o que está sendo divulgado.
**Vantagem:** SpicyLinks para quem trabalha ativamente com marketing de afiliados.
---
### Analytics
**Linktree gratuito:** Apenas total de views e cliques.
**Linktree Pro:** Análise por link, dados demográficos.
**SpicyLinks:** Analytics de cliques por link disponível nos planos pagos. Dados de onde as pessoas chegam à sua página.
**Vantagem:** Similar nos planos pagos de ambas.
---
### Temas visuais
**Linktree:** Dezenas de temas, mas muitos são genéricos e o resultado final costuma parecer igual ao de milhares de outras pessoas.
**SpicyLinks:** Temas desenvolvidos com a estética do criador de conteúdo em mente. Nos planos superiores, temas premium exclusivos. Menos temas no total, mas mais adequados ao nicho.
**Vantagem:** Depende da preferência — Linktree tem mais quantidade, SpicyLinks tem mais adequação ao nicho.
---
### Suporte
**Linktree:** Suporte em inglês, por email, com tempo de resposta que pode demorar dias. Para usuários fora do plano Pro, suporte bastante limitado.
**SpicyLinks:** Suporte em português. Planos Premium têm suporte prioritário e acesso via Telegram.
**Vantagem:** SpicyLinks para quem precisa de suporte em português.
---
### Confiabilidade e moderação
**Linktree:** Plataforma estável e com alta disponibilidade. Porém, por ter conteúdo de qualquer tipo, há riscos de mudanças na política de uso que afetem criadores de conteúdo adulto.
**SpicyLinks:** Moderação de conteúdo garante que a plataforma se mantém organizada. Criadores aprovados na moderação têm mais estabilidade.
**Vantagem:** SpicyLinks para criadores que querem uma plataforma dedicada ao seu tipo de conteúdo.
---
## Quando o Linktree pode ser a escolha certa
Seria desonesto dizer que o Linktree não tem vantagens em nenhum cenário. Faz sentido usar o Linktree se:
- Você já tem conta antiga e não quer migrar o histórico
- Você produz conteúdo de vários nichos completamente diferentes e precisa de muitos temas
- Você tem audiência internacional e quer a plataforma mais reconhecida globalmente
- Você quer começar gratuitamente sem compromisso de nenhum período
Para todos esses casos, o Linktree resolve. Mas para a maioria das criadoras brasileiras, o custo maior (em dólar) e o suporte apenas em inglês são desvantagens reais.
---
## Quando o SpicyLinks é a escolha certa
O SpicyLinks faz mais sentido se:
- Você é criadora de conteúdo e quer uma plataforma feita para o seu nicho
- Você prefere pagar em reais sem variação cambial
- Você trabalha com links de afiliados e quer acompanhar o desempenho de cada um
- Você valoriza suporte em português
- Você quer uma URL com contexto profissional
- Você está começando e quer um plano básico acessível
---
## Migrar do Linktree é difícil?
Não. O processo leva menos de 30 minutos:
1. Crie sua conta no SpicyLinks
2. Adicione os mesmos links que você tem no Linktree
3. Configure o tema visual
4. Envie para moderação e aguarde aprovação
5. Quando aprovada, atualize o link da bio do Instagram e de qualquer outro lugar onde você divulga
Seus seguidores não percebem a mudança — só encontram uma página de links mais organizada.
---
## Conclusão
Para criadoras de conteúdo brasileiras que querem uma plataforma feita para o seu nicho, com preço em reais e suporte em português, o SpicyLinks entrega mais valor pelo dinheiro do que o Linktree.
O Linktree é a escolha padrão por reconhecimento de marca. O SpicyLinks é a escolha inteligente para quem pesquisa antes de decidir.
---
## Leia também
- [Como Configurar Sua Bio para Ganhar Mais Seguidores](/artigos/como-configurar-sua-bio-para-ganhar-mais-seguidores)
- [Os Melhores Links para Colocar na Sua Bio do Instagram](/artigos/os-melhores-links-para-sua-bio-do-instagram)
- [Como Criadores de Conteúdo Ganham Dinheiro com Links de Afiliados](/artigos/como-criadores-de-conteudo-ganham-dinheiro-com-afiliados)
---
**Teste você mesma e compare.**
Crie sua página no SpicyLinks — são 7 dias grátis para avaliar sem compromisso.
[Criar meu SpicyLinks →](https://spicylinks.site/)
---
**Última atualização:** Abril 2026

View File

@ -482,10 +482,182 @@ public class AdminController : Controller
}
}
[HttpPost]
[Route("CreatePage")]
public IActionResult CreatePage()
public async Task<IActionResult> CreatePage(CreatePageViewModel model)
{
return RedirectToAction("ManagePage", new { id = "new" });
var user = await _authService.GetCurrentUserAsync(User);
if (user == null)
return RedirectToAction("Login", "Auth");
// Check if user already has a page
var existingPage = await _userPageService.GetUserPageAsync(user.Id);
if (existingPage != null)
return RedirectToAction("EditPage");
if (!ModelState.IsValid)
{
var categories = await _categoryService.GetAllCategoriesAsync();
var themes = await _themeService.GetAvailableThemesAsync();
ViewBag.Categories = categories;
ViewBag.Themes = themes;
return View(model);
}
// Generate slug if not provided
if (string.IsNullOrEmpty(model.Slug))
{
model.Slug = await _userPageService.GenerateSlugAsync(model.Category, model.DisplayName);
}
// Check if slug is available
if (!await _userPageService.ValidateSlugAsync(model.Category, model.Slug))
{
ModelState.AddModelError("Slug", "Esta URL já está em uso. Tente outra.");
var categories = await _categoryService.GetAllCategoriesAsync();
var themes = await _themeService.GetAvailableThemesAsync();
ViewBag.Categories = categories;
ViewBag.Themes = themes;
return View(model);
}
// Check if user can create the requested number of links
var activeLinksCount = model.Links?.Count ?? 0;
if (!await _userPageService.CanCreateLinksAsync(user.Id, activeLinksCount))
{
ModelState.AddModelError("", "Você excedeu o limite de links do seu plano atual.");
var categories = await _categoryService.GetAllCategoriesAsync();
var themes = await _themeService.GetAvailableThemesAsync();
ViewBag.Categories = categories;
ViewBag.Themes = themes;
return View(model);
}
// Convert ViewModel to UserPage
var userPage = new UserPage
{
UserId = user.Id,
DisplayName = model.DisplayName,
Category = model.Category,
BusinessType = model.BusinessType,
Bio = model.Bio,
Slug = model.Slug,
Theme = await _themeService.GetThemeByNameAsync(model.SelectedTheme) ?? _themeService.GetDefaultTheme(),
Links = model.Links?.Select(l => new LinkItem
{
Title = l.Title,
Url = l.Url,
Description = l.Description,
Icon = l.Icon,
IsActive = true,
Order = model.Links.IndexOf(l)
}).ToList() ?? new List<LinkItem>()
};
// Add social media links
var socialLinks = new List<LinkItem>();
if (!string.IsNullOrEmpty(model.WhatsAppNumber))
{
socialLinks.Add(new LinkItem
{
Title = "WhatsApp",
Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}",
Icon = "fab fa-whatsapp",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.FacebookUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Facebook",
Url = model.FacebookUrl,
Icon = "fab fa-facebook",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.TwitterUrl))
{
socialLinks.Add(new LinkItem
{
Title = "X / Twitter",
Url = model.TwitterUrl,
Icon = "fab fa-x-twitter",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.InstagramUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Instagram",
Url = model.InstagramUrl,
Icon = "fab fa-instagram",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.TiktokUrl))
{
socialLinks.Add(new LinkItem
{
Title = "TikTok",
Url = model.TiktokUrl,
Icon = "fab fa-tiktok",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.PinterestUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Pinterest",
Url = model.PinterestUrl,
Icon = "fab fa-pinterest",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.DiscordUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Discord",
Url = model.DiscordUrl,
Icon = "fab fa-discord",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
if (!string.IsNullOrEmpty(model.KawaiUrl))
{
socialLinks.Add(new LinkItem
{
Title = "Kawai",
Url = model.KawaiUrl,
Icon = "fas fa-heart",
IsActive = true,
Order = userPage.Links.Count + socialLinks.Count
});
}
userPage.Links.AddRange(socialLinks);
await _userPageService.CreatePageAsync(userPage);
TempData["Success"] = "Página criada com sucesso!";
return RedirectToAction("Dashboard");
}
[HttpGet]
@ -665,16 +837,6 @@ public class AdminController : Controller
FileSize = d.FileSize,
UploadedAt = d.UploadedAt
}).ToList() ?? new List<ManageDocumentViewModel>(),
// Social media fields — extracted from Links so the edit form pre-fills correctly
WhatsAppNumber = page.Links?.FirstOrDefault(l => l.Icon?.Contains("whatsapp") == true)?.Url
?.Replace("https://wa.me/", "").Replace("whatsapp://", "") ?? string.Empty,
FacebookUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("facebook") == true)?.Url ?? string.Empty,
InstagramUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("instagram") == true)?.Url ?? string.Empty,
TwitterUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("twitter") == true)?.Url ?? string.Empty,
TiktokUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("tiktok") == true)?.Url ?? string.Empty,
PinterestUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("pinterest") == true)?.Url ?? string.Empty,
DiscordUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("discord") == true)?.Url ?? string.Empty,
KawaiUrl = page.Links?.FirstOrDefault(l => l.Icon?.Contains("kawai") == true)?.Url ?? string.Empty,
AvailableCategories = categories,
AvailableThemes = themes.Where(t => !t.IsPremium || userPlanType.AllowsCustomThemes()).ToList(),
MaxLinksAllowed = userPlanType.GetMaxLinksPerPage(),
@ -730,13 +892,10 @@ public class AdminController : Controller
if (!string.IsNullOrEmpty(model.WhatsAppNumber))
{
var whatsappDigits = model.WhatsAppNumber
.Replace("https://wa.me/", "").Replace("whatsapp://", "")
.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "");
socialLinks.Add(new LinkItem
{
Title = "WhatsApp",
Url = $"https://wa.me/{whatsappDigits}",
Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}",
Icon = "fab fa-whatsapp",
IsActive = true,
Order = currentOrder++
@ -1082,13 +1241,10 @@ public class AdminController : Controller
if (!string.IsNullOrEmpty(model.WhatsAppNumber))
{
var whatsappDigits = model.WhatsAppNumber
.Replace("https://wa.me/", "").Replace("whatsapp://", "")
.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "");
socialLinks.Add(new LinkItem
{
Title = "WhatsApp",
Url = $"https://wa.me/{whatsappDigits}",
Url = $"https://wa.me/{model.WhatsAppNumber.Replace("+", "").Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "")}",
Icon = "fab fa-whatsapp",
IsActive = true,
Order = currentOrder++

View File

@ -35,16 +35,13 @@ Serilog.Debugging.SelfLog.Enable(msg =>
System.Diagnostics.Debug.WriteLine($"[SERILOG SELF] {msg}");
});
var tenantName = builder.Configuration["Tenant:SiteName"] ?? "BCards";
var loggerConfig = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.Enrich.WithEnvironmentName()
.Enrich.WithProcessId()
.Enrich.WithThreadId()
.Enrich.WithProperty("ApplicationName", tenantName)
.Enrich.WithProperty("Tenant", tenantName)
.Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "BCards")
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
.Enrich.WithProperty("Hostname", hostname);
@ -71,15 +68,14 @@ if (isDevelopment)
var openSearchUrl = builder.Configuration["Serilog:OpenSearchUrl"];
if (!string.IsNullOrEmpty(openSearchUrl))
{
var tenantSlug = tenantName.ToLower().Replace("+", "").Replace(" ", "-");
var indexFormat = $"{tenantSlug}-dev-{{0:yyyy-MM}}";
Console.WriteLine($"[OPENSEARCH DEV] Configurando sink → {openSearchUrl} (index: {indexFormat})");
var indexFormat = "b-cards-dev-{0:yyyy-MM}";
// TODO: após confirmar que conecta, restaurar try/catch silencioso (opcional)
try
{
loggerConfig.WriteTo.Async(a => a.OpenSearch(new OpenSearchSinkOptions(new Uri(openSearchUrl))
{
IndexFormat = indexFormat,
AutoRegisterTemplate = false,
AutoRegisterTemplate = false, // Não faz GET / no startup
ModifyConnectionSettings = conn => conn
.RequestTimeout(TimeSpan.FromSeconds(5))
.PingTimeout(TimeSpan.FromSeconds(3)),
@ -96,12 +92,11 @@ if (isDevelopment)
}),
bufferSize: 10000,
blockWhenFull: false);
Console.WriteLine($"[OPENSEARCH DEV] Sink registrado. Erros de envio aparecem como [SERILOG SELF] no console.");
}
else
catch (Exception)
{
Console.WriteLine("[OPENSEARCH DEV] OpenSearchUrl não configurado — sem sink OpenSearch.");
// Falha silenciosa - logs continuam no console e arquivo
}
}
}
else
@ -137,8 +132,7 @@ else
_ => environment
};
var tenantSlugProd = tenantName.ToLower().Replace("+", "").Replace(" ", "-");
var indexFormat = $"{tenantSlugProd}-{envMapping}-{{0:yyyy-MM}}";
var indexFormat = $"b-cards-{envMapping}-{{0:yyyy-MM}}";
try
{
@ -157,10 +151,9 @@ else
Period = TimeSpan.FromSeconds(5),
}), bufferSize: 10000, blockWhenFull: false);
}
catch (Exception ex)
catch (Exception)
{
// OpenSearch é opcional — app continua com console/arquivo
Console.WriteLine($"[OPENSEARCH PROD] Falha ao configurar sink → {openSearchUrl}: {ex.Message}");
// Falha silenciosa em produção - logs continuam no console/arquivo
}
}
}
@ -633,7 +626,7 @@ app.Use(async (context, next) =>
"frame-src 'self' https://accounts.google.com https://login.microsoftonline.com; " +
"object-src 'none'; " +
"base-uri 'self'; " +
"form-action 'self' https://accounts.google.com https://login.microsoftonline.com";
"form-action 'self'";
context.Response.Headers.Append("Content-Security-Policy", csp);
// Load balancer e debugging headers
@ -895,7 +888,11 @@ using (var scope = app.Services.CreateScope())
await themeService.InitializeDefaultThemesAsync();
}
var existingCategories = await categoryService.GetAllCategoriesAsync();
if (!existingCategories.Any())
{
await categoryService.InitializeDefaultCategoriesAsync();
}
Log.Information("Default themes and categories initialized successfully");
}

View File

@ -24,11 +24,11 @@
},
"applicationUrl": "https://localhost:49182;http://localhost:49183"
},
"LuzLinks": {
"LusLinks": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Luzlinks"
"ASPNETCORE_ENVIRONMENT": "Luslinks"
},
"applicationUrl": "https://localhost:49184;http://localhost:49185"
}

View File

@ -1,7 +1,5 @@
using BCards.Web.Configuration;
using BCards.Web.Models;
using BCards.Web.Repositories;
using Microsoft.Extensions.Options;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Text;
@ -12,12 +10,10 @@ namespace BCards.Web.Services;
public class CategoryService : ICategoryService
{
private readonly ICategoryRepository _categoryRepository;
private readonly TenantSettings _tenant;
public CategoryService(ICategoryRepository categoryRepository, IOptions<TenantSettings> tenantOptions)
public CategoryService(ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
_tenant = tenantOptions.Value;
}
public async Task<List<Category>> GetAllCategoriesAsync()
@ -58,39 +54,6 @@ public class CategoryService : ICategoryService
public async Task InitializeDefaultCategoriesAsync()
{
Console.WriteLine($"[CategoryService] DefaultCategories count: {_tenant.DefaultCategories.Count}, SiteName: {_tenant.SiteName}");
// Se tenant tem DefaultCategories configuradas, sempre sincroniza (tenant config é fonte da verdade)
if (_tenant.DefaultCategories.Any())
{
var existing = await _categoryRepository.GetAllActiveAsync();
var existingSlugs = existing.Select(c => c.Slug).ToHashSet();
var configSlugs = _tenant.DefaultCategories
.Select(i => string.IsNullOrWhiteSpace(i.Slug) ? GenerateSlug(i.Name) : i.Slug)
.ToHashSet();
// Remove categorias que não estão mais na config
foreach (var cat in existing.Where(c => !configSlugs.Contains(c.Slug)))
await _categoryRepository.DeleteAsync(cat.Id!);
// Adiciona categorias novas
foreach (var item in _tenant.DefaultCategories)
{
var slug = string.IsNullOrWhiteSpace(item.Slug) ? GenerateSlug(item.Name) : item.Slug;
if (!existingSlugs.Contains(slug))
{
await _categoryRepository.CreateAsync(new Category
{
Name = item.Name,
Slug = slug,
Icon = item.Icon,
Description = item.Description,
SeoKeywords = item.SeoKeywords
});
}
}
return;
}
var categories = await _categoryRepository.GetAllActiveAsync();
if (categories.Any()) return;

View File

@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
namespace BCards.Web.ViewModels;
public class CreatePageViewModel
{
[Required(ErrorMessage = "Nome é obrigatório")]
[StringLength(50, ErrorMessage = "Nome deve ter no máximo 50 caracteres")]
public string DisplayName { get; set; } = string.Empty;
[Required(ErrorMessage = "Categoria é obrigatória")]
public string Category { get; set; } = string.Empty;
[Required(ErrorMessage = "Tipo de negócio é obrigatório")]
public string BusinessType { get; set; } = "individual";
[StringLength(200, ErrorMessage = "Bio deve ter no máximo 200 caracteres")]
public string Bio { get; set; } = string.Empty;
[Required(ErrorMessage = "Tema é obrigatório")]
public string SelectedTheme { get; set; } = "minimalist";
public string WhatsAppNumber { get; set; } = string.Empty;
public string FacebookUrl { get; set; } = string.Empty;
public string TwitterUrl { get; set; } = string.Empty;
public string InstagramUrl { get; set; } = string.Empty;
public string TiktokUrl { get; set; } = string.Empty;
public string PinterestUrl { get; set; } = string.Empty;
public string DiscordUrl { get; set; } = string.Empty;
public string KawaiUrl { get; set; } = string.Empty;
public List<CreateLinkViewModel> Links { get; set; } = new();
public string Slug { get; set; } = string.Empty;
}
public class CreateLinkViewModel
{
[Required(ErrorMessage = "Título é obrigatório")]
[StringLength(50, ErrorMessage = "Título deve ter no máximo 50 caracteres")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "URL é obrigatória")]
[Url(ErrorMessage = "URL inválida")]
public string Url { get; set; } = string.Empty;
[StringLength(100, ErrorMessage = "Descrição deve ter no máximo 100 caracteres")]
public string Description { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
}

View File

@ -18,7 +18,7 @@ public class ManagePageViewModel
[Required(ErrorMessage = "Tipo de negócio é obrigatório")]
public string BusinessType { get; set; } = "individual";
[StringLength(3000, ErrorMessage = "Bio deve ter no máximo 3000 caracteres")]
[StringLength(200, ErrorMessage = "Bio deve ter no máximo 200 caracteres")]
public string Bio { get; set; } = string.Empty;
public string Slug { get; set; } = string.Empty;

View File

@ -0,0 +1,618 @@
@model BCards.Web.ViewModels.CreatePageViewModel
@{
ViewData["Title"] = "Criar Página";
Layout = "_Layout";
}
<div class="container-fluid">
<div class="row">
<div class="col-12 col-lg-8 mx-auto">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">
<i class="fas fa-magic"></i>
Criar Sua Página de Links
</h4>
</div>
<div class="card-body">
<!-- Progress Bar -->
<div class="progress mb-4" style="height: 8px;">
<div class="progress-bar" role="progressbar" style="width: 20%" id="wizardProgress"></div>
</div>
<form asp-action="CreatePage" method="post" id="createPageForm">
<!-- Step 1: Informações Básicas -->
<div class="wizard-step" id="step1">
<h5 class="step-title">
<span class="step-number">1</span>
Informações Básicas
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="DisplayName" class="form-label">Nome da Página</label>
<input asp-for="DisplayName" class="form-control" placeholder="Ex: João Silva">
<span asp-validation-for="DisplayName" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="Category" class="form-label">Categoria</label>
<select asp-for="Category" class="form-select">
<option value="">Selecione uma categoria</option>
@foreach (var category in ViewBag.Categories as List<BCards.Web.Models.Category> ?? new List<BCards.Web.Models.Category>())
{
<option value="@category.Name">@category.Name</option>
}
</select>
<span asp-validation-for="Category" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="BusinessType" class="form-label">Tipo</label>
<select asp-for="BusinessType" class="form-select">
<option value="individual">Pessoa Física</option>
<option value="company">Empresa</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="slugPreview" class="form-label">URL da Página</label>
<div class="input-group">
<span class="input-group-text">page/</span>
<span class="input-group-text" id="categorySlug">categoria</span>
<span class="input-group-text">/</span>
<input type="text" class="form-control" id="slugPreview" readonly>
<input asp-for="Slug" type="hidden">
</div>
<small class="form-text text-muted">URL gerada automaticamente</small>
</div>
</div>
</div>
<div class="mb-3">
<label asp-for="Bio" class="form-label">Bio/Descrição</label>
<textarea asp-for="Bio" class="form-control" rows="3" placeholder="Uma breve descrição sobre você ou sua empresa..."></textarea>
<span asp-validation-for="Bio" class="text-danger"></span>
</div>
</div>
<!-- Step 2: Seleção de Tema -->
<div class="wizard-step d-none" id="step2">
<h5 class="step-title">
<span class="step-number">2</span>
Escolha Seu Tema Visual
</h5>
<div class="row">
@foreach (var theme in ViewBag.Themes as List<BCards.Web.Models.PageTheme> ?? new List<BCards.Web.Models.PageTheme>())
{
<div class="col-md-4 mb-3">
<div class="theme-card" data-theme="@theme.Name.ToLower()">
<div class="theme-preview" style="background: @theme.BackgroundColor; color: @theme.TextColor;">
<div class="theme-header" style="background-color: @theme.PrimaryColor;">
<div class="theme-avatar"></div>
<h6>@theme.Name</h6>
</div>
<div class="theme-links">
<div class="theme-link" style="background-color: @theme.PrimaryColor;"></div>
<div class="theme-link" style="background-color: @theme.SecondaryColor;"></div>
</div>
</div>
<div class="theme-name">
@theme.Name
@if (theme.IsPremium)
{
<span class="badge bg-warning">Premium</span>
}
</div>
</div>
</div>
}
</div>
<input asp-for="SelectedTheme" type="hidden">
</div>
<!-- Step 3: Links Principais -->
<div class="wizard-step d-none" id="step3">
<h5 class="step-title">
<span class="step-number">3</span>
Links Principais
</h5>
<div id="linksContainer">
<!-- Links will be added dynamically -->
</div>
<button type="button" class="btn btn-outline-primary" id="addLinkBtn">
<i class="fas fa-plus"></i> Adicionar Link
</button>
</div>
<!-- Step 4: Redes Sociais -->
<div class="wizard-step d-none" id="step4">
<h5 class="step-title">
<span class="step-number">4</span>
Redes Sociais
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="WhatsAppNumber" class="form-label">
<i class="fab fa-whatsapp text-success"></i>
WhatsApp
</label>
<input asp-for="WhatsAppNumber" class="form-control" placeholder="+55 11 99999-9999">
<span asp-validation-for="WhatsAppNumber" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="FacebookUrl" class="form-label">
<i class="fab fa-facebook text-primary"></i>
Facebook
</label>
<input asp-for="FacebookUrl" class="form-control" placeholder="https://facebook.com/seu-perfil">
<span asp-validation-for="FacebookUrl" class="text-danger"></span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label asp-for="TwitterUrl" class="form-label">
<i class="fab fa-x-twitter"></i>
X / Twitter
</label>
<input asp-for="TwitterUrl" class="form-control" placeholder="https://x.com/seu-perfil">
<span asp-validation-for="TwitterUrl" class="text-danger"></span>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label asp-for="InstagramUrl" class="form-label">
<i class="fab fa-instagram text-danger"></i>
Instagram
</label>
<input asp-for="InstagramUrl" class="form-control" placeholder="https://instagram.com/seu-perfil">
<span asp-validation-for="InstagramUrl" class="text-danger"></span>
</div>
</div>
</div>
</div>
<!-- Step 5: Preview e Finalização -->
<div class="wizard-step d-none" id="step5">
<h5 class="step-title">
<span class="step-number">5</span>
Preview e Finalização
</h5>
<div class="preview-container">
<div class="preview-phone">
<div class="preview-screen" id="previewScreen">
<!-- Preview will be generated here -->
</div>
</div>
</div>
<div class="text-center mt-4">
<p class="text-muted">Sua página estará disponível em:</p>
<strong id="finalUrl">page/categoria/seu-slug</strong>
</div>
</div>
<!-- Navigation Buttons -->
<div class="wizard-navigation mt-4">
<button type="button" class="btn btn-secondary" id="prevBtn" style="display: none;">
<i class="fas fa-arrow-left"></i> Anterior
</button>
<button type="button" class="btn btn-primary float-end" id="nextBtn">
Próximo <i class="fas fa-arrow-right"></i>
</button>
<button type="submit" class="btn btn-success float-end d-none" id="submitBtn">
<i class="fas fa-check"></i> Criar Página
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<style>
.wizard-step {
min-height: 400px;
}
.step-title {
color: #495057;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.step-number {
display: inline-block;
width: 30px;
height: 30px;
line-height: 30px;
background-color: #007bff;
color: white;
border-radius: 50%;
text-align: center;
margin-right: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
}
.theme-card {
cursor: pointer;
border: 2px solid transparent;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.theme-card:hover,
.theme-card.selected {
border-color: #007bff;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.theme-preview {
height: 120px;
position: relative;
padding: 1rem;
}
.theme-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.theme-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
}
.theme-header h6 {
margin: 0;
font-size: 0.75rem;
color: white;
}
.theme-links {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.theme-link {
height: 8px;
border-radius: 4px;
opacity: 0.8;
}
.theme-name {
padding: 0.75rem;
text-align: center;
font-weight: 500;
background-color: #f8f9fa;
}
.link-input-group {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.preview-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
.preview-phone {
width: 300px;
height: 400px;
border: 8px solid #333;
border-radius: 20px;
background-color: #000;
padding: 20px 10px;
position: relative;
}
.preview-screen {
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 12px;
overflow-y: auto;
padding: 1rem;
}
.wizard-navigation {
border-top: 1px solid #dee2e6;
padding-top: 1rem;
}
</style>
<script>
let currentStep = 1;
const totalSteps = 5;
let linkCount = 0;
$(document).ready(function() {
initializeWizard();
// Generate slug when name or category changes
$('#DisplayName, #Category').on('input change', function() {
generateSlug();
});
// Theme selection
$('.theme-card').on('click', function() {
$('.theme-card').removeClass('selected');
$(this).addClass('selected');
const themeName = $(this).data('theme');
$('#SelectedTheme').val(themeName);
});
// Navigation
$('#nextBtn').on('click', function() {
if (validateCurrentStep()) {
nextStep();
}
});
$('#prevBtn').on('click', function() {
prevStep();
});
// Add link functionality
$('#addLinkBtn').on('click', function() {
addLinkInput();
});
// Form submission
$('#createPageForm').on('submit', function(e) {
generateLinksData();
});
});
function initializeWizard() {
updateProgressBar();
updateNavigationButtons();
addLinkInput(); // Add first link input
}
function nextStep() {
if (currentStep < totalSteps) {
$(`#step${currentStep}`).addClass('d-none');
currentStep++;
$(`#step${currentStep}`).removeClass('d-none');
if (currentStep === 5) {
generatePreview();
}
updateProgressBar();
updateNavigationButtons();
}
}
function prevStep() {
if (currentStep > 1) {
$(`#step${currentStep}`).addClass('d-none');
currentStep--;
$(`#step${currentStep}`).removeClass('d-none');
updateProgressBar();
updateNavigationButtons();
}
}
function updateProgressBar() {
const progress = (currentStep / totalSteps) * 100;
$('#wizardProgress').css('width', progress + '%');
}
function updateNavigationButtons() {
$('#prevBtn').toggle(currentStep > 1);
if (currentStep === totalSteps) {
$('#nextBtn').addClass('d-none');
$('#submitBtn').removeClass('d-none');
} else {
$('#nextBtn').removeClass('d-none');
$('#submitBtn').addClass('d-none');
}
}
function validateCurrentStep() {
let isValid = true;
switch (currentStep) {
case 1:
if (!$('#DisplayName').val() || !$('#Category').val()) {
alert('Por favor, preencha o nome e a categoria.');
isValid = false;
}
break;
case 2:
if (!$('#SelectedTheme').val()) {
alert('Por favor, selecione um tema.');
isValid = false;
}
break;
}
return isValid;
}
function generateSlug() {
const name = $('#DisplayName').val();
const category = $('#Category').val();
if (name && category) {
$.post('/Admin/GenerateSlug', { category: category, name: name })
.done(function(data) {
$('#Slug').val(data.slug);
$('#slugPreview').val(data.slug);
$('#categorySlug').text(category);
$('#finalUrl').text(`page/${category}/${data.slug}`);
});
}
}
function addLinkInput() {
linkCount++;
const linkHtml = `
<div class="link-input-group" data-link="${linkCount}">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="mb-0">Link ${linkCount}</h6>
<button type="button" class="btn btn-sm btn-outline-danger remove-link-btn">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-2">
<label class="form-label">Título</label>
<input type="text" class="form-control link-title" placeholder="Ex: Meu Site">
</div>
</div>
<div class="col-md-6">
<div class="mb-2">
<label class="form-label">URL</label>
<input type="url" class="form-control link-url" placeholder="https://exemplo.com">
</div>
</div>
</div>
<div class="mb-2">
<label class="form-label">Descrição (opcional)</label>
<input type="text" class="form-control link-description" placeholder="Breve descrição do link">
</div>
</div>
`;
$('#linksContainer').append(linkHtml);
// Add remove functionality
$('.remove-link-btn').off('click').on('click', function() {
$(this).closest('.link-input-group').remove();
});
}
function generateLinksData() {
const links = [];
$('.link-input-group').each(function() {
const title = $(this).find('.link-title').val();
const url = $(this).find('.link-url').val();
const description = $(this).find('.link-description').val();
if (title && url) {
links.push({
Title: title,
Url: url,
Description: description,
Icon: ''
});
}
});
// Remove existing hidden link inputs
$('input[name^="Links["]').remove();
// Create hidden inputs for links directly in the form
links.forEach((link, index) => {
$('#createPageForm').append(`
<input type="hidden" name="Links[${index}].Title" value="${link.Title}" />
<input type="hidden" name="Links[${index}].Url" value="${link.Url}" />
<input type="hidden" name="Links[${index}].Description" value="${link.Description}" />
<input type="hidden" name="Links[${index}].Icon" value="${link.Icon}" />
`);
});
// Debug: Log what we're sending
console.log('=== DEBUG GENERATELINKSDATA ===');
console.log('Links found:', links.length);
links.forEach((link, index) => {
console.log(`Link ${index}:`, link);
});
console.log('=== FIM DEBUG ===');
}
function generatePreview() {
const name = $('#DisplayName').val();
const bio = $('#Bio').val();
const selectedTheme = $('#SelectedTheme').val();
let previewHtml = `
<div class="text-center">
<div class="mb-3">
<div style="width: 60px; height: 60px; background-color: #ddd; border-radius: 50%; margin: 0 auto;"></div>
</div>
<h5 class="mb-2">${name}</h5>
<p class="text-muted small mb-3">${bio}</p>
<div class="d-grid gap-2">
`;
// Add links preview
$('.link-input-group').each(function() {
const title = $(this).find('.link-title').val();
if (title) {
previewHtml += `<div class="btn btn-primary btn-sm">${title}</div>`;
}
});
// Add social media preview
if ($('#WhatsAppNumber').val()) {
previewHtml += `<div class="btn btn-success btn-sm"><i class="fab fa-whatsapp"></i> WhatsApp</div>`;
}
if ($('#FacebookUrl').val()) {
previewHtml += `<div class="btn btn-primary btn-sm"><i class="fab fa-facebook"></i> Facebook</div>`;
}
if ($('#TwitterUrl').val()) {
previewHtml += `<div class="btn btn-dark btn-sm"><i class="fab fa-x-twitter"></i> X / Twitter</div>`;
}
if ($('#InstagramUrl').val()) {
previewHtml += `<div class="btn btn-danger btn-sm"><i class="fab fa-instagram"></i> Instagram</div>`;
}
previewHtml += `</div></div>`;
$('#previewScreen').html(previewHtml);
}
</script>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

View File

@ -115,54 +115,9 @@
<div class="mb-3">
<label asp-for="Bio" class="form-label">Bio/Descrição</label>
<div class="md-toolbar border rounded-top border-bottom-0 bg-light px-2 py-1 d-flex gap-1 flex-wrap position-relative">
<button type="button" class="btn btn-sm btn-outline-secondary md-btn" data-target="Bio" data-wrap="**" title="Negrito"><b>B</b></button>
<button type="button" class="btn btn-sm btn-outline-secondary md-btn" data-target="Bio" data-wrap="*" title="Itálico"><i>I</i></button>
<button type="button" class="btn btn-sm btn-outline-secondary md-list-btn" data-target="Bio" title="Lista">&#8226; Lista</button>
<button type="button" class="btn btn-sm btn-outline-secondary md-link-btn" data-target="Bio" title="Link">&#128279; Link</button>
<button type="button" class="btn btn-sm btn-outline-secondary md-icon-picker-btn" data-target="Bio" title="Inserir ícone">&#9881; Ícone</button>
<div class="md-icon-picker-panel shadow border rounded bg-white p-2" style="display:none; position:absolute; top:100%; left:0; z-index:9999; width:300px; max-height:260px; overflow-y:auto;">
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Status</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="OK">✅</span><span class="md-icon-item" title="Erro">❌</span><span class="md-icon-item" title="Atenção">⚠️</span><span class="md-icon-item" title="Info"></span><span class="md-icon-item" title="Check">✔</span><span class="md-icon-item" title="X">✘</span><span class="md-icon-item" title="Seta">➤</span><span class="md-icon-item" title="Seta direita">→</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Formas &amp; Bullets</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Bullet">•</span><span class="md-icon-item" title="Círculo vazio">○</span><span class="md-icon-item" title="Círculo cheio">●</span><span class="md-icon-item" title="Quadrado cheio">■</span><span class="md-icon-item" title="Quadrado vazio">□</span><span class="md-icon-item" title="Quadrado com check">☑</span><span class="md-icon-item" title="Losango cheio">◆</span><span class="md-icon-item" title="Losango vazio">◇</span><span class="md-icon-item" title="Triângulo">▶</span><span class="md-icon-item" title="Estrela">★</span><span class="md-icon-item" title="Estrela vazia">☆</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Círculos coloridos</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Vermelho">🔴</span><span class="md-icon-item" title="Laranja">🟠</span><span class="md-icon-item" title="Amarelo">🟡</span><span class="md-icon-item" title="Verde">🟢</span><span class="md-icon-item" title="Azul">🔵</span><span class="md-icon-item" title="Roxo">🟣</span><span class="md-icon-item" title="Preto">⚫</span><span class="md-icon-item" title="Branco">⚪</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Negócios &amp; Escritório</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Gráfico barras">📊</span><span class="md-icon-item" title="Gráfico alta">📈</span><span class="md-icon-item" title="Gráfico baixa">📉</span><span class="md-icon-item" title="Prancheta">📋</span><span class="md-icon-item" title="Pin">📌</span><span class="md-icon-item" title="Pasta">📁</span><span class="md-icon-item" title="Maleta">💼</span><span class="md-icon-item" title="Nota">📝</span><span class="md-icon-item" title="Email">📧</span><span class="md-icon-item" title="Telefone">📞</span><span class="md-icon-item" title="Alvo">🎯</span><span class="md-icon-item" title="Troféu">🏆</span>
</div>
</div>
<div class="md-icon-section mb-2">
<div class="text-muted small mb-1 fw-semibold">Tecnologia</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Notebook">💻</span><span class="md-icon-item" title="Monitor">🖥️</span><span class="md-icon-item" title="Celular">📱</span><span class="md-icon-item" title="Teclado">⌨️</span><span class="md-icon-item" title="Impressora">🖨️</span><span class="md-icon-item" title="Engrenagem">⚙️</span><span class="md-icon-item" title="Lupa">🔍</span><span class="md-icon-item" title="Link">🔗</span>
</div>
</div>
<div class="md-icon-section">
<div class="text-muted small mb-1 fw-semibold">Finanças &amp; Outros</div>
<div class="d-flex flex-wrap gap-1">
<span class="md-icon-item" title="Dinheiro">💰</span><span class="md-icon-item" title="Cartão">💳</span><span class="md-icon-item" title="Banco">🏦</span><span class="md-icon-item" title="Chave">🔑</span><span class="md-icon-item" title="Cadeado">🔒</span><span class="md-icon-item" title="Sino">🔔</span><span class="md-icon-item" title="Lâmpada">💡</span><span class="md-icon-item" title="Raio">⚡</span><span class="md-icon-item" title="Pessoa">👤</span><span class="md-icon-item" title="Pessoas">👥</span>
</div>
</div>
</div>
</div>
<textarea asp-for="Bio" id="Bio" class="form-control rounded-0 rounded-bottom" rows="5" maxlength="3000" placeholder="Uma breve descrição sobre você ou sua empresa..." style="font-family: monospace; font-size: 0.9rem;"></textarea>
<textarea asp-for="Bio" class="form-control" rows="3" placeholder="Uma breve descrição sobre você ou sua empresa..."></textarea>
<span asp-validation-for="Bio" class="text-danger"></span>
<div class="form-text">Máximo 3000 caracteres. Use **negrito**, *itálico*, - item para listas.</div>
<div class="form-text">Máximo 200 caracteres</div>
</div>
<!-- Profile Image Upload -->
@ -1255,6 +1210,7 @@
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script>
const LINK_TYPES_CONFIG = @Html.Raw(linkTypesJson);
let linkCount = @Model.Links.Count;
@ -1263,9 +1219,6 @@
const totalSteps = 5;
$(document).ready(function() {
// Initialize Markdown toolbar
initMarkdownToolbar();
// Initialize social media fields
initializeSocialMedia();
@ -1384,11 +1337,11 @@
updateLinkNumbers();
});
// Loading state — só desabilita se a validação JS não bloqueou o submit
// Form validation
$('#managePageForm').on('submit', function(e) {
if (!e.isDefaultPrevented()) {
$(this).find('button[type="submit"]').prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>Salvando...');
}
console.log('Form submitted');
// Allow submission but add loading state
$(this).find('button[type="submit"]').prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>Criando...');
});
});
@ -1990,84 +1943,6 @@
}, 7000);
}
// Markdown Toolbar
function initMarkdownToolbar() {
// Icon picker toggle
document.querySelectorAll('.md-icon-picker-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var panel = this.parentElement.querySelector('.md-icon-picker-panel');
var isVisible = panel.style.display !== 'none';
document.querySelectorAll('.md-icon-picker-panel').forEach(function(p) { p.style.display = 'none'; });
if (!isVisible) panel.style.display = 'block';
});
});
// Insert icon on click
document.querySelectorAll('.md-icon-item').forEach(function(item) {
item.addEventListener('click', function(e) {
e.stopPropagation();
var panel = this.closest('.md-icon-picker-panel');
var btn = panel.parentElement.querySelector('.md-icon-picker-btn');
var targetId = btn ? btn.dataset.target : 'Bio';
var ta = document.getElementById(targetId);
var icon = this.textContent;
var start = ta.selectionStart, end = ta.selectionEnd;
ta.value = ta.value.substring(0, start) + icon + ta.value.substring(end);
ta.selectionStart = ta.selectionEnd = start + icon.length;
ta.focus();
panel.style.display = 'none';
});
});
// Close picker on outside click
document.addEventListener('click', function() {
document.querySelectorAll('.md-icon-picker-panel').forEach(function(p) { p.style.display = 'none'; });
});
document.querySelectorAll('.md-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var targetId = this.dataset.target;
var wrap = this.dataset.wrap;
var ta = document.getElementById(targetId);
var start = ta.selectionStart, end = ta.selectionEnd;
var sel = ta.value.substring(start, end) || 'texto';
var before = ta.value.substring(0, start);
var after = ta.value.substring(end);
ta.value = before + wrap + sel + wrap + after;
ta.selectionStart = start + wrap.length;
ta.selectionEnd = start + wrap.length + sel.length;
ta.focus();
});
});
document.querySelectorAll('.md-list-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var targetId = this.dataset.target;
var ta = document.getElementById(targetId);
var start = ta.selectionStart;
var lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
var before = ta.value.substring(0, lineStart);
var after = ta.value.substring(lineStart);
ta.value = before + '- ' + after;
ta.selectionStart = ta.selectionEnd = start + 2;
ta.focus();
});
});
document.querySelectorAll('.md-link-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var targetId = this.dataset.target;
var ta = document.getElementById(targetId);
var start = ta.selectionStart, end = ta.selectionEnd;
var sel = ta.value.substring(start, end) || 'texto do link';
var url = prompt('URL do link:') || 'https://';
var before = ta.value.substring(0, start);
var after = ta.value.substring(end);
var md = '[' + sel + '](' + url + ')';
ta.value = before + md + after;
ta.selectionStart = ta.selectionEnd = start + md.length;
ta.focus();
});
});
}
// Validation Error Handling
function checkValidationErrors() {
// Só verificar erros se estamos em um POST-back (ou seja, se ModelState foi validado)
@ -2387,10 +2262,8 @@
}
// Atualizar campo hidden - SEMPRE string, nunca null
// WhatsApp: armazena só o número (servidor adiciona https://wa.me/ ao salvar)
// Outros: armazena URL completa (servidor usa diretamente)
if (value) {
hiddenField.val(isWhatsApp ? value : prefix + value);
hiddenField.val(prefix + value);
} else {
hiddenField.val(' '); // Espaço em branco para evitar null
}
@ -2733,16 +2606,6 @@
@section Styles {
<style>
.md-icon-item {
cursor: pointer;
font-size: 1.2rem;
padding: 2px 4px;
border-radius: 4px;
line-height: 1.4;
transition: background 0.1s;
}
.md-icon-item:hover { background: #e9ecef; }
/* Estilo customizado para o scroll dos temas */
.themes-container {
scrollbar-width: thin;

View File

@ -7,75 +7,29 @@
Layout = "_Layout";
}
<div class="container-fluid px-3 px-lg-5 py-5">
<div class="container py-5">
<div class="text-center mb-5">
<h1 class="display-5 fw-bold mb-3">Escolha o plano ideal para você</h1>
<p class="lead text-muted">Comece grátis e faça upgrade quando precisar de mais recursos</p>
<!-- Toggle Mensal/Anual -->
<div class="d-flex justify-content-center mb-4 mt-3">
<div class="pricing-toggle-wrapper">
<span class="pricing-savings-chip">🎁 2 meses grátis</span>
<div class="pricing-pill">
<button type="button" class="pill-btn pill-active" id="btnMonthly">Mensal</button>
<button type="button" class="pill-btn" id="btnYearly">Anual</button>
<div class="d-flex justify-content-center mb-4">
<div class="btn-group" role="group" aria-label="Período de cobrança">
<input type="radio" class="btn-check" name="billingPeriod" id="monthly" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="monthly">Mensal</label>
<input type="radio" class="btn-check" name="billingPeriod" id="yearly" autocomplete="off">
<label class="btn btn-outline-primary" for="yearly">
Anual
<span class="badge bg-success ms-1">2 meses grátis</span>
</label>
</div>
</div>
</div>
<style>
.pricing-toggle-wrapper {
position: relative;
display: inline-block;
}
.pricing-savings-chip {
position: absolute;
top: -24px;
right: -4px;
background: #198754;
color: #fff;
font-size: 0.68rem;
font-weight: 700;
padding: 2px 9px;
border-radius: 20px;
white-space: nowrap;
letter-spacing: 0.02em;
}
.pricing-pill {
display: inline-flex;
background: #ededf3;
border-radius: 50px;
padding: 4px;
gap: 2px;
}
.pill-btn {
padding: 8px 32px;
border-radius: 50px;
border: none;
background: transparent;
color: #555;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: background 0.18s, color 0.18s, box-shadow 0.18s;
white-space: nowrap;
}
.pill-btn.pill-active {
background: var(--tenant-primary, #667eea);
color: #fff;
box-shadow: 0 2px 10px rgba(0,0,0,.18);
}
.pricing-cards .card-body {
font-size: 0.875rem;
}
.pricing-cards .card-header h5 {
font-size: 0.95rem;
}
</style>
</div>
<div class="row g-4 row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-5 pricing-cards">
<div class="row g-3 justify-content-center pricing-cards">
<!-- Plano Trial -->
<div class="col">
<div class="col-xl-2 col-lg-4 col-md-6">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-success bg-opacity-10 text-center py-4">
<h5 class="mb-0">Trial Gratuito</h5>
@ -111,7 +65,7 @@
<div class="card-footer bg-transparent p-4">
@if (User.Identity?.IsAuthenticated == true)
{
<a asp-controller="Admin" asp-action="ManagePage" asp-route-id="new" class="btn btn-success w-100">Começar Grátis</a>
<a asp-controller="Admin" asp-action="CreatePage" class="btn btn-success w-100">Começar Grátis</a>
}
else
{
@ -122,7 +76,7 @@
</div>
<!-- Plano Básico -->
<div class="col">
<div class="col-xl-2 col-lg-4 col-md-6">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-light text-center py-4">
<h5 class="mb-0">Básico</h5>
@ -196,7 +150,7 @@
</div>
<!-- Plano Profissional (Decoy) -->
<div class="col">
<div class="col-xl-2 col-lg-4 col-md-6">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-warning bg-opacity-10 text-center py-4">
<h5 class="mb-0">Profissional</h5>
@ -270,10 +224,12 @@
</div>
<!-- Plano Premium (Mais Popular) -->
<div class="col">
<div class="card h-100 border-primary shadow">
<div class="card-header bg-primary text-white text-center pt-3 pb-4">
<span class="badge bg-white text-primary px-3 py-1 mb-2 d-inline-block" style="font-size:0.7rem;letter-spacing:.04em;">⭐ Mais Popular</span>
<div class="col-xl-2 col-lg-4 col-md-6">
<div class="card h-100 border-primary shadow position-relative">
<div class="position-absolute top-0 start-50 translate-middle">
<span class="badge bg-primary px-3 py-2">Mais Popular</span>
</div>
<div class="card-header bg-primary text-white text-center py-4">
<h5 class="mb-0">Premium</h5>
<div class="mt-3">
<div class="pricing-monthly">
@ -353,10 +309,12 @@
</div>
<!-- Plano Premium + Afiliados -->
<div class="col">
<div class="col-xl-2 col-lg-4 col-md-6">
<div class="card h-100 border-success shadow">
<div class="card-header bg-success text-white text-center pt-3 pb-4">
<span class="badge bg-white text-success px-3 py-1 mb-2 d-inline-block" style="font-size:0.7rem;letter-spacing:.04em;">🆕 Novo!</span>
<div class="position-absolute top-0 start-50 translate-middle pricing-premium-badge">
<span class="badge bg-success px-3 py-2">Novo!</span>
</div>
<div class="card-header bg-success text-white text-center py-4">
<h5 class="mb-0">Premium + Afiliados</h5>
<div class="mt-3">
<div class="pricing-monthly">
@ -436,7 +394,6 @@
</div>
</div>
<div class="container">
<!-- Comparação de recursos -->
<div class="mt-5 pt-5">
<h2 class="text-center mb-4">Compare todos os recursos</h2>
@ -578,7 +535,6 @@
</div>
</div>
</div>
</div><!-- /container inner -->
</div>
@if (TempData["Success"] != null)
@ -631,20 +587,22 @@
<script>
document.addEventListener('DOMContentLoaded', function() {
const btnMonthly = document.getElementById('btnMonthly');
const btnYearly = document.getElementById('btnYearly');
const monthlyRadio = document.getElementById('monthly');
const yearlyRadio = document.getElementById('yearly');
const monthlyElements = document.querySelectorAll('.pricing-monthly');
const yearlyElements = document.querySelectorAll('.pricing-yearly');
function setPricing(period) {
const isYearly = period === 'yearly';
btnMonthly.classList.toggle('pill-active', !isYearly);
btnYearly.classList.toggle('pill-active', isYearly);
monthlyElements.forEach(el => el.classList.toggle('d-none', isYearly));
yearlyElements.forEach(el => el.classList.toggle('d-none', !isYearly));
function togglePricing() {
if (yearlyRadio.checked) {
monthlyElements.forEach(el => el.classList.add('d-none'));
yearlyElements.forEach(el => el.classList.remove('d-none'));
} else {
monthlyElements.forEach(el => el.classList.remove('d-none'));
yearlyElements.forEach(el => el.classList.add('d-none'));
}
}
btnMonthly.addEventListener('click', () => setPricing('monthly'));
btnYearly.addEventListener('click', () => setPricing('yearly'));
monthlyRadio.addEventListener('change', togglePricing);
yearlyRadio.addEventListener('change', togglePricing);
});
</script>

View File

@ -103,11 +103,9 @@
<div class="card-body">
@if (!string.IsNullOrEmpty(Model.Page.Bio))
{
var bioPipeline = new Markdig.MarkdownPipelineBuilder().UseAutoLinks().DisableHtml().Build();
var bioHtml = Markdig.Markdown.ToHtml(Model.Page.Bio, bioPipeline);
<div class="mb-3">
<strong>Biografia:</strong>
<div>@Html.Raw(bioHtml)</div>
<p>@Model.Page.Bio</p>
</div>
}

View File

@ -44,27 +44,6 @@
@await RenderSectionAsync("Styles", required: false)
<style>
:root {
--tenant-primary: @tenant.PrimaryColor;
--tenant-primary-dark: @tenant.PrimaryColorDark;
--tenant-gradient: @tenant.HeroGradient;
}
.hero-section { background: var(--tenant-gradient) !important; }
.btn-primary { background-color: var(--tenant-primary) !important; border-color: var(--tenant-primary) !important; }
.btn-primary:hover { background-color: var(--tenant-primary-dark) !important; border-color: var(--tenant-primary-dark) !important; }
.btn-outline-primary { color: var(--tenant-primary) !important; border-color: var(--tenant-primary) !important; }
.btn-outline-primary:hover { background-color: var(--tenant-primary) !important; color: #fff !important; }
.text-primary { color: var(--tenant-primary) !important; }
.bg-primary { background-color: var(--tenant-primary) !important; }
.border-primary { border-color: var(--tenant-primary) !important; }
.bg-primary.bg-opacity-10 { background-color: color-mix(in srgb, var(--tenant-primary) 10%, transparent) !important; }
#loading-bar { background: linear-gradient(90deg, var(--tenant-primary), var(--tenant-primary-dark), var(--tenant-primary)) !important; }
.nav-link.active { background-color: color-mix(in srgb, var(--tenant-primary) 10%, transparent) !important; }
.form-control:focus { border-color: var(--tenant-primary); box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--tenant-primary) 25%, transparent); }
.profile-image-placeholder { border-color: var(--tenant-primary) !important; }
</style>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
@ -99,14 +78,14 @@
100% { background-position: 200% 0; }
}
/* Destacar item ativo do menu — usa cor do tenant */
/* Destacar item ativo do menu */
.nav-link.active {
background-color: color-mix(in srgb, var(--tenant-primary, #0d6efd) 10%, transparent) !important;
background-color: rgba(0, 123, 255, 0.1) !important;
border-radius: 6px !important;
font-weight: 600 !important;
}
/* Para homepage (fundo colorido) */
/* Para homepage (fundo azul) */
.bg-home-blue .nav-link.active {
background-color: rgba(255, 255, 255, 0.2) !important;
}
@ -157,8 +136,7 @@
<div id="loading-bar"></div>
<header>
<nav class="navbar navbar-expand-lg navbar-toggleable-sm navbar-light fixed-top @(ViewBag.IsHomePage == true ? "bg-home-blue" : "bg-dashboard")" id="mainNavbar"
style="@(ViewBag.IsHomePage == true ? $"background-color: {tenant.PrimaryColor} !important;" : "")">
<nav class="navbar navbar-expand-lg navbar-toggleable-sm navbar-light fixed-top @(ViewBag.IsHomePage == true ? "bg-home-blue" : "bg-dashboard")" id="mainNavbar">
<div class="container-fluid">
<a class="navbar-brand fw-bold @(ViewBag.IsHomePage == true ? "text-white" : "text-primary")"
asp-area="" asp-controller="Home" asp-action="Index">

View File

@ -176,16 +176,7 @@
text-overflow: ellipsis;
}
/* Seta de expansão e Botão de copiar */
.link-actions-container {
display: flex;
align-items: center;
gap: 0.5rem;
margin-left: 0.5rem;
flex-shrink: 0;
}
.copy-link-btn,
/* Seta de expansão */
.expand-arrow {
background: rgba(255, 255, 255, 0.2);
border: none;
@ -198,15 +189,15 @@
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
margin-left: 0.5rem;
}
.copy-link-btn:hover,
.expand-arrow:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
.copy-link-btn i,
.expand-arrow i {
font-size: 0.9rem;
transition: transform 0.3s ease;
@ -291,20 +282,6 @@
gap: 0.25rem;
}
.expanded-link {
color: var(--primary-color) !important;
text-decoration: underline !important;
display: flex;
align-items: center;
gap: 0.5rem;
transition: opacity 0.2s ease;
}
.expanded-link:hover {
opacity: 0.8;
text-decoration: none !important;
}
/* ========== FOOTER ========== */
.profile-footer {
margin-top: 2rem;

View File

@ -209,37 +209,31 @@
.qrcode-toggle {
width: 100%;
background-color: var(--primary-color);
color: white !important;
border: none;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 12px;
padding: 0.75rem 1.25rem;
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.3s ease;
color: inherit;
font-size: 1rem;
font-weight: 600;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
font-weight: 500;
}
.qrcode-toggle:hover {
background-color: var(--secondary-color);
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.qrcode-toggle i {
transition: transform 0.3s ease;
}
.qrcode-toggle div {
display: flex;
align-items: center;
gap: 0.75rem;
}
.qrcode-container {
margin-top: 1rem;
padding: 1.5rem;
@ -367,9 +361,7 @@
@if (!string.IsNullOrEmpty(Model.Bio))
{
var bioPipeline = new Markdig.MarkdownPipelineBuilder().UseAutoLinks().DisableHtml().Build();
var bioHtml = Markdig.Markdown.ToHtml(Model.Bio, bioPipeline);
<div class="profile-bio">@Html.Raw(bioHtml)</div>
<p class="profile-bio">@Model.Bio</p>
}
<!-- Links Container -->
@ -384,7 +376,7 @@
var hasExpandableContent = (!string.IsNullOrEmpty(link.Description) ||
(link.Type == BCards.Web.Models.LinkType.Product && !string.IsNullOrEmpty(link.ProductDescription)));
<div class="universal-link" id="link-@i" data-link-id="@i">
<div class="universal-link" data-link-id="@i">
<a href="@NormalizeSocialUrl(link.Url, link.Icon)"
class="universal-link-header"
onclick="recordClick('@Model.Id', @i)"
@ -432,13 +424,6 @@
</div>
</div>
<div class="link-actions-container">
<button class="copy-link-btn"
type="button"
title="Copiar link"
onclick="event.preventDefault(); event.stopPropagation(); copyAnchorLink('link-@i', this)">
<i class="fas fa-copy"></i>
</button>
@if (hasExpandableContent)
{
<button class="expand-arrow"
@ -447,7 +432,6 @@
<i class="fas fa-chevron-down"></i>
</button>
}
</div>
</a>
@if (hasExpandableContent)
@ -479,10 +463,8 @@
}
}
<div class="expanded-action">
<a href="@NormalizeSocialUrl(link.Url, link.Icon)" target="_blank" rel="noopener noreferrer" class="expanded-link">
<i class="fas fa-external-link-alt"></i>
Clique aqui para abrir o link
</a>
Clique no título acima para abrir
</div>
</div>
}
@ -535,10 +517,8 @@
<div class="universal-link-details" id="details-@uniqueId">
<div class="expanded-description">@document.Description</div>
<div class="expanded-action">
<a href="/api/document/@document.FileId" target="_blank" rel="noopener noreferrer" class="expanded-link">
<i class="fas fa-external-link-alt"></i>
Clique aqui para abrir o PDF
</a>
Clique no título acima para abrir o PDF
</div>
</div>
}
@ -558,13 +538,13 @@
<div class="qrcode-section mt-4">
<button class="qrcode-toggle" onclick="toggleQRCode()" type="button">
<i class="fas fa-qrcode me-2"></i>
QR Code
<span id="qrToggleText">Ocultar QR Code</span>
<i class="fas fa-chevron-up ms-auto" id="qrToggleIcon"></i>
</button>
<div class="qrcode-container" id="qrcodeContainer" style="display: block;">
<div class="qrcode-canvas" id="qrcode"></div>
<p class="qrcode-hint">Escaneie o QR Code para abrir no celular</p>
<p class="qrcode-hint">Escaneie para compartilhar esta página</p>
<button class="btn-download-qr" onclick="downloadQR()" type="button">
<i class="fas fa-download me-1"></i> Baixar QR Code
</button>
@ -602,8 +582,8 @@
</div>
@section Scripts {
<!-- QR Code Library - Usando bwip-js para maior compatibilidade -->
<script src="https://cdn.jsdelivr.net/npm/bwip-js@3.4.1/dist/bwip-js-min.js"></script>
<!-- QRCode.js Library - Load FIRST -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
function recordClick(pageId, linkIndex) {
@ -673,129 +653,72 @@
// Generate QR Code on page load
generateQRCode();
// Auto-open link from hash
if (window.location.hash) {
const targetId = window.location.hash.substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
// Small delay to ensure everything is rendered
setTimeout(() => {
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
// If it has a link-id, try to open the details
const linkId = targetElement.getAttribute('data-link-id');
const docId = targetElement.getAttribute('data-document-id');
if (linkId !== null) {
toggleLinkDetails(linkId);
} else if (docId !== null) {
toggleLinkDetails(docId);
}
}, 500);
}
}
});
function copyAnchorLink(id, btn) {
// Preserva a URL completa incluindo query strings (importante para o preview token)
// e apenas substitui/adiciona o hash
const url = new URL(window.location.href);
url.hash = id;
const fullUrl = url.toString();
navigator.clipboard.writeText(fullUrl).then(() => {
const icon = btn.querySelector('i');
const originalClass = icon.className;
icon.className = 'fas fa-check text-success';
btn.classList.add('copied');
setTimeout(() => {
icon.className = originalClass;
btn.classList.remove('copied');
}, 2000);
}).catch(err => {
console.error('Erro ao copiar link:', err);
});
}
// QR Code Functions
let qrCodeGenerated = false;
function toggleQRCode() {
const container = document.getElementById('qrcodeContainer');
const icon = document.getElementById('qrToggleIcon');
const text = document.getElementById('qrToggleText');
if (container.style.display === 'block') {
// Close
container.style.display = 'none';
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
text.textContent = 'Mostrar QR Code';
} else {
// Open
container.style.display = 'block';
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
text.textContent = 'Ocultar QR Code';
}
}
function generateQRCode() {
if (qrCodeGenerated) {
console.log('[QR] QR Code already generated, skipping...');
console.log('QR Code already generated, skipping...');
return;
}
const qrcodeElement = document.getElementById("qrcode");
if (!qrcodeElement) {
console.error('[QR] QR Code container not found');
console.error('QR Code container not found');
return;
}
if (typeof bwipjs === 'undefined') {
console.error('[QR] bwip-js library is not loaded');
qrcodeElement.innerHTML = '<p class="text-danger small">Erro ao carregar gerador de QR Code. Verifique sua conexão ou bloqueador de anúncios.</p>';
// Check if already has content (double-call prevention)
if (qrcodeElement.querySelector('canvas')) {
console.log('Canvas already exists, skipping generation');
qrCodeGenerated = true;
return;
}
// Construir a URL final (LivePage)
const baseUrl = window.location.origin;
const categoryName = '@Html.Raw(category?.Name ?? "")'.trim();
const slug = '@Html.Raw(Model.Slug ?? "")'.trim();
// Mark as generated BEFORE creating to prevent race conditions
qrCodeGenerated = true;
// Se não houver categoria, usa 'geral' ou remove a barra extra
const categoryPart = categoryName ? encodeURIComponent(categoryName) : "page";
const pageUrl = `${baseUrl}/page/${categoryPart}/${encodeURIComponent(slug)}`;
// Clear any existing content
qrcodeElement.innerHTML = '';
console.log('[QR] Generating QR for URL:', pageUrl);
// Criar um elemento canvas dinamicamente
const canvas = document.createElement('canvas');
canvas.style.maxWidth = '100%';
canvas.style.height = 'auto';
const pageUrl = window.location.href.split('?')[0]; // Remove query params
try {
// bwip-js usa um canvas para renderizar
bwipjs.toCanvas(canvas, {
bcid: 'qrcode', // Tipo de código de barras
text: pageUrl, // Texto/URL
scale: 3, // Escala (resolução)
height: 50, // Altura (para QR é proporcional)
width: 50,
includetext: false, // Não mostrar o texto embaixo
textxalign: 'center',
new QRCode(qrcodeElement, {
text: pageUrl,
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
// Limpar e adicionar o canvas ao container
qrcodeElement.innerHTML = '';
qrcodeElement.appendChild(canvas);
qrCodeGenerated = true;
console.log('[QR] QR Code generated successfully with bwip-js');
console.log('QR Code generated successfully for:', pageUrl);
} catch (error) {
console.error('[QR] Error generating QR Code with bwip-js:', error);
qrcodeElement.innerHTML = '<p class="text-danger small">Erro ao gerar QR Code. Consulte o console para detalhes.</p>';
qrCodeGenerated = false;
console.error('Error generating QR Code:', error);
qrcodeElement.innerHTML = '<p class="text-danger small">Erro ao gerar QR Code</p>';
qrCodeGenerated = false; // Reset on error so it can retry
}
}

View File

@ -1,4 +1,3 @@
@using BCards.Web
@using BCards.Web.Models
@using Markdig
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -14,7 +14,7 @@
"Environment": "test"
},
"Serilog": {
"OpenSearchUrl": "http://192.168.0.100:9200"
"OpenSearchUrl": "http://192.168.0.100:9200",
},
"DetailedErrors": true,
"MongoDb": {

View File

@ -6,22 +6,18 @@
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543",
"Environment": "test"
},
"Serilog": {
"OpenSearchUrl": "http://192.168.0.100:9200"
},
"MongoDb": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "LuzLinksDB"
"DatabaseName": "LusLinksDB"
},
"Tenant": {
"SiteName": "LuzLinks",
"SiteName": "LusLinks",
"SiteDescription": "A plataforma para pastores, padres, líderes religiosos e ministérios. Reúna seus estudos bíblicos, eventos, lives e canal em uma única página de fé.",
"Tagline": "Conecte sua comunidade em um único link",
"SupportEmail": "suporte@luzlinks.site",
"ContentFolder": "luzlinks",
"SupportEmail": "suporte@luslinks.site",
"ContentFolder": "luslinks",
"AgeGated": false,
"UrlExample": "luzlinks.site/pastor/seu-nome",
"DpoEmail": "dpo@luzlinks.site",
"UrlExample": "luslinks.site/pastor/seu-nome",
"DpoEmail": "dpo@luslinks.site",
"HeroHeadline": "Conecte sua comunidade em um único link",
"HeroDescription": "A plataforma ideal para pastores, padres, líderes e ministérios. Reúna seus estudos bíblicos, agenda de cultos, canal e dízimos em uma só página.",
"HeroCtaText": "Criar Minha Bio de Fé",
@ -32,25 +28,10 @@
{ "Icon": "📅", "Title": "Agenda e Eventos", "Description": "Compartilhe retiros, cultos especiais e eventos com toda a comunidade de forma simples e organizada." }
],
"CtaHeadline": "Compartilhe sua mensagem com o mundo",
"CtaDescription": "Líderes de toda denominação já usam o LuzLinks para alcançar mais pessoas com sua mensagem de fé.",
"CtaDescription": "Líderes de toda denominação já usam o LusLinks para alcançar mais pessoas com sua mensagem de fé.",
"CtaButtonText": "Criar Minha Bio de Fé",
"MetaKeywords": "bio links pastor, página ministério, linktree cristão, links religiosos, página iglesia, bio pastor, links igreja",
"FooterTagline": "Conectando fé e comunidade.",
"HeroGradient": "linear-gradient(135deg, #5b9bd5 0%, #1a5276 100%)",
"PrimaryColor": "#2471a3",
"PrimaryColorDark": "#1a5276",
"DefaultCategories": [
{ "Icon": "🙏", "Name": "Pastores", "Slug": "pastor", "Description": "Pastores evangélicos, protestantes e pentecostais", "SeoKeywords": [ "pastor", "evangélico", "protestante", "pentecostal", "pregador" ] },
{ "Icon": "✝️", "Name": "Padres", "Slug": "padre", "Description": "Sacerdotes, padres e religiosos da Igreja Católica", "SeoKeywords": [ "padre", "sacerdote", "católico", "pároco", "religioso" ] },
{ "Icon": "⛪", "Name": "Igrejas", "Slug": "igreja", "Description": "Congregações, comunidades de fé e denominações", "SeoKeywords": [ "igreja", "congregação", "comunidade", "denominação", "templo" ] },
{ "Icon": "🌟", "Name": "Ministérios", "Slug": "ministerio", "Description": "Ministérios, organizações e missões cristãs", "SeoKeywords": [ "ministério", "missão", "organização cristã", "obra" ] },
{ "Icon": "🎵", "Name": "Louvor e Adoração", "Slug": "louvor", "Description": "Ministérios de louvor, bandas gospel e cantores cristãos", "SeoKeywords": [ "louvor", "adoração", "gospel", "banda", "música cristã" ] },
{ "Icon": "👨‍👩‍👧", "Name": "Família e Jovens", "Slug": "familia", "Description": "Líderes de grupos de jovens, casais e família", "SeoKeywords": [ "jovens", "família", "casais", "célula", "grupo" ] },
{ "Icon": "📖", "Name": "Estudos Bíblicos", "Slug": "estudos", "Description": "Mestres, professores bíblicos e teólogos", "SeoKeywords": [ "estudo bíblico", "teologia", "mestre", "professor", "bíblia" ] },
{ "Icon": "🌍", "Name": "Missionários", "Slug": "missionario","Description": "Missionários e evangelistas nacionais e internacionais", "SeoKeywords": [ "missionário", "evangelista", "evangelismo", "missões" ] },
{ "Icon": "📻", "Name": "Mídia Cristã", "Slug": "midia", "Description": "Podcasts, canais, rádios e comunicação cristã", "SeoKeywords": [ "podcast", "canal cristão", "rádio evangélica", "mídia" ] },
{ "Icon": "🤝", "Name": "Assistência Social", "Slug": "assistencia","Description": "Projetos sociais, pastorais de assistência e ONGs cristãs","SeoKeywords": [ "assistência social", "projeto social", "ONG", "pastoral" ] }
],
"AllowedLinkTypes": [
{ "Icon": "fas fa-globe", "Label": "🌐 Site / Ministério", "Prefix": "https://", "Placeholder": "ministerio.com.br", "Instructions": "Digite o domínio do site", "Color": "bg-primary" },
{ "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "contato@ministerio.com", "Instructions": "Digite apenas o email", "Color": "bg-success" },
@ -65,7 +46,7 @@
]
},
"SendGrid": {
"FromEmail": "noreply@luzlinks.site",
"FromName": "LuzLinks"
"FromEmail": "noreply@luslinks.site",
"FromName": "LusLinks"
}
}

View File

@ -7,13 +7,8 @@
"Environment": "test"
},
"MongoDb": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "SpicyLinksDB"
},
"Serilog": {
"OpenSearchUrl": "http://192.168.0.100:9200"
},
"Tenant": {
"SiteName": "SpicyLinks",
"SiteDescription": "A plataforma discreta e segura para criadores de conteúdo adulto. Reúna suas assinaturas, lista de desejos, redes sociais e conteúdo exclusivo em uma única bio.",
@ -28,153 +23,24 @@
"HeroCtaText": "Criar Minha Bio",
"FeaturesHeadline": "Por que criadores escolhem o {SiteName}?",
"Features": [
{
"Icon": "❤️",
"Title": "Tudo num Só Link",
"Description": "Instagram, Twitter/X, OnlyFans, lista de desejos e mais — tudo em uma bio única, elegante e fácil de compartilhar."
},
{
"Icon": "🔒",
"Title": "Verificação de Idade",
"Description": "Acesso protegido com verificação de idade automática. Plataforma segura, discreta e responsável."
},
{
"Icon": "📊",
"Title": "Saiba Quem Te Visita",
"Description": "Analytics detalhado de cliques, visualizações e origem do tráfego para otimizar suas conversões."
}
{ "Icon": "❤️", "Title": "Tudo num Só Link", "Description": "Instagram, Twitter/X, OnlyFans, lista de desejos e mais — tudo em uma bio única, elegante e fácil de compartilhar." },
{ "Icon": "🔒", "Title": "Verificação de Idade", "Description": "Acesso protegido com verificação de idade automática. Plataforma segura, discreta e responsável." },
{ "Icon": "📊", "Title": "Saiba Quem Te Visita", "Description": "Analytics detalhado de cliques, visualizações e origem do tráfego para otimizar suas conversões." }
],
"CtaHeadline": "Pronta para monetizar seu conteúdo?",
"CtaDescription": "Milhares de criadoras já centralizam seus links e aumentam suas conversões com o SpicyLinks.",
"CtaButtonText": "Criar Minha Bio",
"MetaKeywords": "bio links criadora, creator bio, linktree conteudo adulto, links onlyfans, bio instagram criadora",
"FooterTagline": "Seu conteúdo, sua identidade.",
"HeroGradient": "linear-gradient(135deg, #ff416c 0%, #c0392b 100%)",
"PrimaryColor": "#e63946",
"PrimaryColorDark": "#c1121f",
"DefaultCategories": [
{
"Icon": "📸",
"Name": "Modelos",
"Slug": "modelos",
"Description": "Modelos e criadores de conteúdo visual",
"SeoKeywords": [ "modelo", "fotografia", "conteúdo", "criadora" ]
},
{
"Icon": "⭐",
"Name": "Influencers",
"Slug": "influencers",
"Description": "Influencers e personalidades digitais",
"SeoKeywords": [ "influencer", "digital", "social media" ]
},
{
"Icon": "💪",
"Name": "Fitness",
"Slug": "fitness",
"Description": "Criadores de conteúdo fitness e lifestyle",
"SeoKeywords": [ "fitness", "academia", "saúde", "corpo" ]
},
{
"Icon": "🎨",
"Name": "Arte",
"Slug": "arte",
"Description": "Artistas e criadores de conteúdo visual",
"SeoKeywords": [ "arte", "ilustração", "design", "criativo" ]
},
{
"Icon": "🎵",
"Name": "Música",
"Slug": "musica",
"Description": "Músicos e cantores independentes",
"SeoKeywords": [ "música", "cantor", "artista", "show" ]
},
{
"Icon": "🎮",
"Name": "Gaming",
"Slug": "gaming",
"Description": "Streamers e criadores de conteúdo gamer",
"SeoKeywords": [ "gaming", "streamer", "games", "twitch" ]
},
{
"Icon": "🦸",
"Name": "Cosplay",
"Slug": "cosplay",
"Description": "Cosplayers e criadores de fantasia",
"SeoKeywords": [ "cosplay", "anime", "fantasia", "cosplayer" ]
},
{
"Icon": "💋",
"Name": "Lifestyle",
"Slug": "lifestyle",
"Description": "Criadores de conteúdo lifestyle e entretenimento",
"SeoKeywords": [ "lifestyle", "entretenimento", "diversão" ]
}
],
"AllowedLinkTypes": [
{
"Icon": "fas fa-globe",
"Label": "🌐 Site Geral",
"Prefix": "https://",
"Placeholder": "exemplo.com",
"Instructions": "Digite o domínio e caminho",
"Color": "bg-primary"
},
{
"Icon": "fas fa-envelope",
"Label": "✉️ Email",
"Prefix": "mailto:",
"Placeholder": "seuemail@exemplo.com",
"Instructions": "Digite apenas o email",
"Color": "bg-success"
},
{
"Icon": "fas fa-phone",
"Label": "📞 Telefone",
"Prefix": "tel:",
"Placeholder": "5511999999999",
"Instructions": "Número com código do país",
"Color": "bg-success"
},
{
"Icon": "fab fa-instagram",
"Label": "📸 Instagram",
"Prefix": "https://instagram.com/",
"Placeholder": "seu.usuario",
"Instructions": "Digite apenas seu usuário",
"Color": "bg-danger"
},
{
"Icon": "fab fa-twitter",
"Label": "🐦 Twitter/X",
"Prefix": "https://x.com/",
"Placeholder": "seu_usuario",
"Instructions": "Digite apenas seu usuário",
"Color": "bg-dark"
},
{
"Icon": "fab fa-tiktok",
"Label": "🎵 TikTok",
"Prefix": "https://tiktok.com/@",
"Placeholder": "seu.usuario",
"Instructions": "Digite apenas seu usuário",
"Color": "bg-dark"
},
{
"Icon": "fas fa-shopping-cart",
"Label": "🛒 Lista de Desejos",
"Prefix": "https://",
"Placeholder": "wishlist.com/...",
"Instructions": "Link para lista de desejos",
"Color": "bg-warning"
},
{
"Icon": "fas fa-heart",
"Label": "❤️ Assinatura",
"Prefix": "https://",
"Placeholder": "plataforma.com/...",
"Instructions": "Link para plataforma paga",
"Color": "bg-danger"
}
{ "Icon": "fas fa-globe", "Label": "🌐 Site Geral", "Prefix": "https://", "Placeholder": "exemplo.com", "Instructions": "Digite o domínio e caminho", "Color": "bg-primary" },
{ "Icon": "fas fa-envelope", "Label": "✉️ Email", "Prefix": "mailto:", "Placeholder": "seuemail@exemplo.com", "Instructions": "Digite apenas o email", "Color": "bg-success" },
{ "Icon": "fas fa-phone", "Label": "📞 Telefone", "Prefix": "tel:", "Placeholder": "5511999999999", "Instructions": "Número com código do país", "Color": "bg-success" },
{ "Icon": "fab fa-instagram", "Label": "📸 Instagram", "Prefix": "https://instagram.com/","Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-danger" },
{ "Icon": "fab fa-twitter", "Label": "🐦 Twitter/X", "Prefix": "https://x.com/", "Placeholder": "seu_usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-dark" },
{ "Icon": "fab fa-tiktok", "Label": "🎵 TikTok", "Prefix": "https://tiktok.com/@", "Placeholder": "seu.usuario", "Instructions": "Digite apenas seu usuário", "Color": "bg-dark" },
{ "Icon": "fas fa-shopping-cart","Label": "🛒 Lista de Desejos","Prefix": "https://", "Placeholder": "wishlist.com/...", "Instructions": "Link para lista de desejos", "Color": "bg-warning" },
{ "Icon": "fas fa-heart", "Label": "❤️ Assinatura", "Prefix": "https://", "Placeholder": "plataforma.com/...", "Instructions": "Link para plataforma paga", "Color": "bg-danger" }
]
},
"SendGrid": {

View File

@ -21,7 +21,7 @@
"Plans": {
"Basic": {
"Name": "Básico",
"PriceId": "price_1TR10MBk8jHwC3c0iey23Ghb",
"PriceId": "price_1RycPaBMIadsOxJVKioZZofK",
"Price": 12.90,
"MaxPages": 3,
"MaxLinks": 8,
@ -35,7 +35,7 @@
},
"Professional": {
"Name": "Profissional",
"PriceId": "price_1TR10NBk8jHwC3c0yqmy8soD",
"PriceId": "price_1RycQmBMIadsOxJVGqjVMaOj",
"Price": 25.90,
"MaxPages": 5,
"MaxLinks": 20,
@ -49,7 +49,7 @@
},
"Premium": {
"Name": "Premium",
"PriceId": "price_1TR10OBk8jHwC3c0eZa77y31",
"PriceId": "price_1RycRUBMIadsOxJVkxGOh3uu",
"Price": 29.90,
"MaxPages": 15,
"MaxLinks": -1,
@ -64,7 +64,7 @@
},
"PremiumAffiliate": {
"Name": "Premium+Afiliados",
"PriceId": "price_1TR10PBk8jHwC3c0B1oIvvYY",
"PriceId": "price_1RycTaBMIadsOxJVeDLseXQq",
"Price": 34.90,
"MaxPages": 15,
"MaxLinks": -1,
@ -79,7 +79,7 @@
},
"BasicYearly": {
"Name": "Básico Anual",
"PriceId": "price_1TR10NBk8jHwC3c0L4SDaWe9",
"PriceId": "price_1RycWgBMIadsOxJVGdtEeoMS",
"Price": 129.00,
"MaxPages": 3,
"MaxLinks": 8,
@ -93,7 +93,7 @@
},
"ProfessionalYearly": {
"Name": "Profissional Anual",
"PriceId": "price_1TR10OBk8jHwC3c0IuyvrvRf",
"PriceId": "price_1RycXdBMIadsOxJV5cNX7dHm",
"Price": 259.00,
"MaxPages": 5,
"MaxLinks": 20,
@ -107,7 +107,7 @@
},
"PremiumYearly": {
"Name": "Premium Anual",
"PriceId": "price_1TR10PBk8jHwC3c0qngPYMUN",
"PriceId": "price_1RycYnBMIadsOxJVPdKmzy4m",
"Price": 299.00,
"MaxPages": 15,
"MaxLinks": -1,
@ -122,7 +122,7 @@
},
"PremiumAffiliateYearly": {
"Name": "Premium+Afiliados Anual",
"PriceId": "price_1TR10QBk8jHwC3c0f8CBaD1n",
"PriceId": "price_1RycaEBMIadsOxJVEhsdB2Y1",
"Price": 349.00,
"MaxPages": 15,
"MaxLinks": -1,
@ -147,7 +147,7 @@
},
"Microsoft": {
"ClientId": "b411606a-e574-4f59-b7cd-10dd941b9fa3",
"ClientSecret": "bff10c42-f1e5-487b-bacb-16b1b691aa7d"
"ClientSecret": ".v88Q~2UIFu926J9lETzY_dY16Wqxo0QvYECjdvx"
}
},
"Moderation": {
@ -193,9 +193,6 @@
"CtaButtonText": "Criar Minha Página Grátis",
"MetaKeywords": "cartão digital, página de links, bio links, linktree brasil, página profissional, corretor, advogado, médico, consultor",
"FooterTagline": "Sua presença digital profissional, simplificada.",
"HeroGradient": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
"PrimaryColor": "#667eea",
"PrimaryColorDark": "#5a6fd6",
"AllowedLinkTypes": [
{ "Icon": "fas fa-globe", "Label": "🌐 Site Geral", "Prefix": "https://", "Placeholder": "exemplo.com", "Instructions": "Digite apenas o domínio e caminho (sem https://)", "Color": "bg-primary" },
{ "Icon": "fas fa-shopping-cart", "Label": "🛒 Loja/E-commerce", "Prefix": "https://", "Placeholder": "minhaloja.com/produto", "Instructions": "Digite apenas o domínio e caminho da sua loja", "Color": "bg-success" },

View File

@ -143,9 +143,9 @@ body > .container-fluid {
transition: all 0.3s ease;
}
/* Menu Home — cor sólida primária do tenant (gradiente em elemento estreito parece diferente do hero) */
/* Menu Home (gradiente) */
.bg-home-blue {
background-color: var(--tenant-primary, #667eea) !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
}
.bg-home-blue .navbar-brand,
@ -185,6 +185,6 @@ body > .container-fluid {
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.bg-home-blue .navbar-collapse {
background-color: color-mix(in srgb, var(--tenant-primary, #764ba2) 85%, black);
background-color: rgba(118, 75, 162, 0.95); /* Cor do gradiente para o menu recolhido */
}
}