Documentação Técnica
Guias e referências sobre arquitetura, segurança e boas práticas
Voltar ao Blog
Novidade
18 de novembro de 2025
Home Completamente Renovada
A página inicial do projeto recebeu melhorias significativas em design, performance e experiência do usuário.
✨ Principais Melhorias Implementadas:
-
✨ Hero centralizada e moderna com gradientes animados
Layout redesenhado com gradientes flutuantes, efeitos glassmorphism e animações CSS suaves.
-
🎬 Animações AOS progressivas em todo o conteúdo
Biblioteca AOS integrada com delays progressivos para criar movimento natural ao fazer scroll.
-
📱 Layout totalmente responsivo (até 290px de largura)
Otimizado para qualquer dispositivo, incluindo smartwatches e telas extremamente pequenas.
-
🎯 Conteúdo minimalista focado em conversão
Cards simplificados, CTAs proeminentes e remoção de informações técnicas excessivas da home.
-
📚 Conteúdo técnico reorganizado em /docs/tech
Artigos sobre stack tecnológica, fluxo de requisição e arquitetura MVC movidos para documentação técnica.
-
🚀 Experiência visual fluida e profissional
Tipografia consistente, espaçamentos harmônicos e transições suaves em todos os elementos interativos.
Tecnologias Utilizadas na Renovação:
AOS 2.3.1
CSS Animations
Glassmorphism
Responsive Grid
💡 Acesse: Visite a
página inicial para ver todas as melhorias em ação!
Introdução
18 de novembro de 2025
O que é o System?
System é um projeto educacional open-source construído em PHP 8.3+
que demonstra conceitos modernos de desenvolvimento web através de um sistema multiusuário funcional.
Diferente de tutoriais convencionais, este projeto se explica enquanto funciona.
Cada página que você visita, cada funcionalidade que você usa, está documentada no próprio código-fonte
e no README do repositório.
💡 Filosofia: O código é didático, com comentários explicativos e estrutura clara que serve como referência para desenvolvedores iniciantes e intermediários.
Stack
18 de novembro de 2025
Tecnologias Utilizadas
Stack moderna e robusta para aplicações web profissionais.
Backend
- ✅ PHP 8.3+ — Linguagem moderna com typed properties e match expressions
- ✅ PDO + MySQL 8.0 — Banco de dados relacional com prepared statements
- ✅ Twig 3.0 — Motor de templates seguro e expressivo
- ✅ phpdotenv — Gerenciamento de variáveis de ambiente
- ✅ Composer — Autoloading PSR-4 e gerenciamento de dependências
Frontend
- ✅ Bootstrap 5.3 — Framework CSS responsivo com variáveis CSS
- ✅ Bootstrap Icons — Biblioteca de ícones vetoriais
- ✅ HTML5 Semântico — Estrutura acessível e moderna
- ✅ CSS Custom Properties — Temas dinâmicos claro/escuro
- ✅ JavaScript Vanilla — Sem dependências de frameworks frontend
Arquitetura
18 de novembro de 2025
Como Funciona uma Requisição?
Entenda o ciclo completo desde a URL digitada até o HTML renderizado.
Fluxo de Requisição HTTP
-
Servidor Web (Apache/Nginx) recebe a requisição (ex:
GET /blog)
-
public/index.php (front controller) é executado via rewrite rules
-
Bootstrap carrega configurações, conecta ao banco e inicializa sessão
-
Router encontra a rota correspondente em
routes/web.php
-
Controller processa a lógica (ex:
HomeController@blog)
-
Model consulta o banco de dados MySQL (se necessário)
-
Twig renderiza o template com os dados retornados
-
HTML final é enviado de volta ao navegador do usuário
🎯 Padrão MVC: Model (dados) → Controller (lógica) → View (apresentação)
Observabilidade
23 de novembro de 2025
Monitor de Performance no Painel
Instrumentamos o ciclo completo de requisições do painel para registrar tempo total, segmentos
(roteador, middlewares, dispatch) e quantidade de queries executadas pelo PDO, tudo sem depender de frameworks externos.
Como ativar: defina as variáveis abaixo no .env (já presentes no projeto):
PERF_MONITOR=true
PERF_SLOW_MS=600
PERF_LOG_PATH="/var/www/system/storage/logs/perf.log"
PERF_QUERY_SAMPLES=5
O que é coletado?
- Duração total da requisição e marcação de requests "lentas" (>
PERF_SLOW_MS).
- Segmentos detalhados:
router.match, router.middleware, router.dispatch e outros que adicionarmos.
- Pico de memória (MB) e contagem total de queries executadas via PDO.
- Até
PERF_QUERY_SAMPLES SQLs completas com duração em milissegundos.
Onde visualizar?
Os eventos são gravados em storage/logs/perf.log como JSON lines. Cada linha contém:
- metadata: URI normalizada, método HTTP, rota atendida e status final.
- segments: tempos por etapa em milissegundos.
- query_samples: SQL compactado + duração para troubleshooting rápido.
Exemplo de entrada (abreviado)
{
"timestamp": "2025-11-23T14:05:10Z",
"duration_ms": 742.18,
"is_slow": true,
"query_count": 38,
"segments": [
{"name": "router.match", "ms": 4.1},
{"name": "router.middleware", "ms": 12.3},
{"name": "router.dispatch", "ms": 698.4}
],
"query_samples": [
{"sql": "SELECT * FROM posts WHERE status = 'published' ORDER BY...", "ms": 83.4}
]
}
Dica: reduza PERF_SLOW_MS em ambientes de desenvolvimento para pegar gargalos menores e desative o monitor em produção se não precisar dos logs contínuos.
PHP
Arquitetura
15 de novembro de 2025
Como Funciona o Sistema de Roteamento
Entenda como o Router custom mapeia URLs para Controllers e métodos específicos.
O roteamento é o coração de qualquer framework web. No System, usamos um Router simples mas poderoso
que mapeia requisições HTTP para ações de controllers.
Exemplo de Rota em routes/web.php:
[
'path' => '/login',
'method' => 'POST',
'handler' => 'AuthController@login'
]
Quando você envia um POST /login, o Router:
- Itera sobre todas as rotas definidas
- Compara
$route['path'] e $route['method'] com a requisição
- Quando encontra match, chama
dispatch('AuthController@login')
- Dispatch instancia
AuthController e executa login()
💡 Dica: Veja o código completo em app/Core/Router.php
Arquitetura
Painel Admin
21 de novembro de 2025
Painel Administrativo Modularizado
O antigo AdminController monolítico foi fatiado em módulos menores. Agora cada área
(usuários, posts e galeria) possui seu próprio controller especializado, enquanto as regras
compartilhadas vivem em um AdminBaseController reutilizável.
Nova árvore em app/Controllers/Admin/
Admin/
├── AdminBaseController.php # CSRF, flashes, slug helpers, preferências
├── UsersAdminController.php # CRUD e aprovação de usuários
├── PostsAdminController.php # Posts, categorias, tags e notificações
└── GalleryAdminController.php# Imagens e categorias da galeria
AdminBaseController
Centraliza helpers como enforceCsrf(), flashes de sessão,
normalização de status, geração de slugs e preferências do usuário. O
AdminController e todos os submódulos o estendem diretamente.
AdminController enxuto
Foca no dashboard, conta do usuário logado, preferências e widgets de
servidor. Ele instancia os controllers especializados e delega os fluxos CRUD.
UsersAdminController
- CRUD completo de usuários
- Uploads/remoção de avatar
- Atualiza sessões quando o usuário edita o próprio perfil
PostsAdminController
- Posts, categorias e tags
- Controle de status + notificações internas
- Upload/substituição de covers com
PostCoverUploader
GalleryAdminController
- Uploads via
GalleryImageUploader
- Gerência de categorias da galeria
- Validações de formulários e flashes dedicados
Como o Router enxerga
As rotas existentes continuam apontando para AdminController.
Ele atua como façade, validando permissões e repassando a chamada para o módulo correto.
Assim não foi necessário alterar routes/web.php ou o JavaScript do dashboard.
Quando útil, podemos expor rotas diretas (ex:
'handler' => 'Admin\\PostsAdminController@postsStore') sem quebrar nada.
Benefícios imediatos:
- Cada controller ficou com ~200-300 linhas, facilitando manutenção
- Helper únicos (CSRF, flashes, slugs) agora têm uma fonte da verdade
- Fica mais simples testar e mover cada módulo para rotas/menus separados
- Prepara o terreno para dividir
dashboard.twig em seções menores
Atualização
23 de novembro de 2025
Dashboard 100% modularizado
Refatoração Completa do Dashboard
O arquivo views/dashboard.twig foi completamente modularizado. Todas as seções foram extraídas para partials Twig reutilizáveis,
macros foram criados para reduzir duplicação, e o JavaScript foi consolidado em um bundle otimizado.
Seções Extraídas
11 partials
views/dashboard/sections/
Navbar Modular
4 partials
views/dashboard/navbar/
Macros Twig
3 arquivos
tables, flash, badges
JS Bundle
2 tags script
config + bundle.js
🧭 Modularização do Navbar
O navbar do dashboard (~180 linhas) foi dividido em componentes independentes para facilitar manutenção e testes isolados.
Estrutura Modular
navbar.twig |
Orquestrador (33 linhas) |
navbar/breadcrumb.twig |
Navegação hierárquica (5 linhas) |
navbar/theme-toggle.twig |
Botão alternar tema (3 linhas) |
navbar/notifications.twig |
Dropdown de notificações (57 linhas) |
navbar/user-menu.twig |
Menu do usuário logado (70 linhas) |
Benefícios
- Redução drástica: Dashboard.twig perdeu ~180 linhas, agora apenas
{% include 'dashboard/navbar.twig' %}
- Manutenção isolada: Alterar layout de notificações não afeta breadcrumb ou menu do usuário
- Reusabilidade: User-menu pode ser reutilizado em outras áreas admin
- Testes independentes: Cada componente pode ser testado visualmente sem carregar dashboard completo
- AB Testing: Facilita testar novos layouts de notificações ou menus
- Uso de macros: User-menu já utiliza
badges.role() em vez de dicionário duplicado
🧩 Modularização da Sidebar
A sidebar do dashboard (~145 linhas) foi modularizada em uma arquitetura granular avançada para facilitar customizações por role (admin, editor, etc.) e manutenção independente de cada grupo de itens do menu.
Estrutura Granular
sidebar.twig |
Orquestrador principal (4 linhas) |
| Componentes Estruturais: |
sidebar/header.twig |
Brand + mini variant (12 linhas) |
sidebar/nav-items.twig |
Orquestrador de itens (7 linhas) |
sidebar/footer.twig |
Badge env + links docs (12 linhas) |
| Itens de Menu (Customizáveis): |
items/overview.twig |
Dashboard home (6 linhas) |
items/admin-section.twig |
Menu admin-only (30 linhas) |
items/posts-dropdown.twig |
Dropdown gestão posts (27 linhas) |
items/gallery-dropdown.twig |
Dropdown gestão galeria (33 linhas) |
items/user-section.twig |
Menu conta/docs (20 linhas) |
Benefícios
- Evolução estrutural: Dashboard passou de arquivo monolítico (~2.600 linhas) para orquestrador modular (atualmente ~323 linhas + partials)
- Menus personalizados por role: Fácil criar sidebar específica para moderadores, revisores, etc.
- Granularidade avançada: Cada grupo lógico de itens em arquivo separado (admin, posts, gallery, user)
- Manutenção isolada: Alterar menu de galeria não afeta menu de posts ou admin
- AB Testing: Testar novos layouts de dropdown sem afetar outros itens
- Facilita expansão: Adicionar nova seção = criar novo arquivo em items/
- Estrutura consistente: Todos os componentes seguem o mesmo padrão de classes CSS e JavaScript
Customização por Role:
Para criar menu específico por role, basta:
- Criar novo arquivo em
sidebar/items/moderator-section.twig
- Adicionar condição em
nav-items.twig: {% if isModerator(me) %}{% include 'dashboard/sidebar/items/moderator-section.twig' %}{% endif %}
- Utilizar mesma estrutura
<li class="sidebar-nav-item"><a href="#section" class="sidebar-nav-link"...>
📁 Estrutura de Partials Twig
Cada seção do dashboard foi extraída para um partial dedicado em views/dashboard/sections/.
Isso reduziu drasticamente o tamanho do arquivo principal e facilitou manutenção isolada.
| Partial |
Responsabilidade |
Dados Utilizados |
overview.twig |
Dashboard inicial com KPIs e resumo |
users, posts, pending_posts, gallery_images |
users.twig |
Tabela de usuários + modais criar/editar |
users, flash_users, csrf_token |
posts.twig |
Lista, criar e editar posts (3 seções consolidadas) |
posts, categories, tags, post_tags_map, flash_posts |
categories.twig |
Gerenciamento de categorias do blog |
categories, flash_categories |
gallery.twig |
Upload e lista de imagens da galeria |
gallery_images, gallery_categories, flash_gallery |
system.twig |
Informações do ambiente PHP e servidor |
session, funções PHP nativas |
database.twig |
Schema do banco (users, posts, categories, tags, gallery) |
Nenhum (documentação estática) |
routes.twig |
Listagem de todas as rotas registradas |
Nenhum (documentação estática) |
logs.twig |
Debug de sessão e variáveis de servidor |
session, server |
account.twig |
Perfil do usuário logado + modal editar |
me, flash_account |
docs.twig |
Portal de documentação interna |
Nenhum (links estáticos) |
🧩 Macros Twig Reutilizáveis
Três arquivos de macros foram criados para eliminar duplicação de código e padronizar componentes visuais.
tables.twig
Macro: table(id, headers, rows, classes)
Renderiza tabelas com thead/tbody padrão. Usado em users, posts e categories.
flash.twig
Macro: render(flash, attr='')
Centraliza alertas de flash messages com suporte a tipos: success, danger, warning, info.
badges.twig
Macro: role(role)
Renderiza badges de papéis de usuário (admin, editor, author, subscriber) com ícones e cores consistentes.
Subscriber
⚡ Modularização JavaScript
O JavaScript do dashboard foi consolidado em um sistema de bundle que carrega todos os módulos na ordem correta,
reduzindo de 9 tags <script> para apenas 2.
Arquivos Criados
config.js |
Define window.dashboardConfig global |
bundle.js |
Carrega sequencialmente todos os módulos JS |
Módulos Carregados (bundle, em ordem):
config.js — inicialização
theme.js — modo escuro/claro
sidebar.js — navegação entre seções
tables.js — busca e paginação
modals.js — pré-preenchimento
posts.js — editor Quill
notifications.js — polling
notifications-center.js — dropdown
account.js — avatar preview
main.js — orquestrador final
Módulo adicional: dogs-modals.js é carregado separadamente via defer no template do dashboard.
bundle.js
Carregador dinâmico e sequencial:
(function() {
function withBase(path) { /* usa meta app-base-url */ }
const modules = [
'js/dashboard/config.js',
'js/dashboard/theme.js',
'js/dashboard/sidebar.js',
'js/dashboard/tables.js',
'js/dashboard/modals.js',
'js/dashboard/posts.js',
'js/dashboard/notifications.js',
'js/dashboard/notifications-center.js',
'js/dashboard/account.js',
'js/dashboard/main.js'
];
modules.forEach((src) => {
const script = document.createElement('script');
script.src = withBase(src);
script.async = false; // Preserva ordem
document.head.appendChild(script);
});
})();
📊 Uso no Template
Antes (9 scripts inline):
<script>
var dashboardConfig = { ... };
</script>
<script src="/js/dashboard/sidebar.js"></script>
<script src="/js/dashboard/tables.js"></script>
<script src="/js/dashboard/theme.js"></script>
<script src="/js/dashboard/posts.js"></script>
<script src="/js/dashboard/notifications.js"></script>
<script src="/js/dashboard/notifications-center.js"></script>
<script src="/js/dashboard/account.js"></script>
<script src="/js/dashboard/modals.js"></script>
<script src="/js/dashboard/main.js"></script>
Depois (2 scripts otimizados):
<script>
// Injeção server-side de configuração
window.dashboardConfig = {
isAdmin: false,
canManagePosts: false,
isEditor: false,
currentUserId: null,
csrfToken: 'a818bc1a1b4e78dd9cabd972ee276dc13c11e463f01c0e78e4bdee6ca3f881bf'
};
</script>
<script src="/js/dashboard/bundle.js" defer></script>
<script src="/js/dashboard/dogs-modals.js" defer></script>
✅ Benefícios da Modularização Completa:
- Manutenibilidade: Cada partial/macro tem responsabilidade única e pode ser testado isoladamente
- Reusabilidade: Macros de tabela, flash e badges podem ser usados em outros templates
- Performance: Bundle.js com
defer não bloqueia renderização; módulos carregam em paralelo
- DRY: Eliminou duplicação de role badges (users/account) e flash messages (6 seções)
- Escalabilidade: Adicionar nova seção exige apenas criar partial e incluir no dashboard.twig
- Cache: Navegador pode cachear bundle.js e macros independentemente do template principal
📦 Estrutura Final do Projeto:
views/
├── dashboard.twig (orquestrador principal - ~323 linhas)
└── dashboard/
├── navbar.twig (navbar modular)
├── navbar/
│ ├── breadcrumb.twig
│ ├── theme-toggle.twig
│ ├── notifications.twig
│ └── user-menu.twig
├── sidebar.twig (sidebar modular)
├── sidebar/
│ ├── header.twig
│ ├── footer.twig
│ ├── nav-items.twig
│ └── items/
│ ├── overview.twig
│ ├── admin-section.twig
│ ├── users-dropdown.twig
│ ├── posts-dropdown.twig
│ ├── dogs-dropdown.twig
│ ├── gallery-dropdown.twig
│ └── user-section.twig
└── sections/
├── overview.twig
├── users.twig
├── posts.twig
├── categories.twig
├── gallery.twig
├── dogs.twig
├── dog-breeds.twig
├── dog-form.twig
├── system.twig
├── database.twig
├── routes.twig
├── logs.twig
├── account.twig
├── docs.twig
├── tables.twig (macro)
├── flash.twig (macro)
└── badges.twig (macro)
public/js/dashboard/
├── bundle.js (loader)
├── config.js (global state)
├── theme.js
├── sidebar.js
├── tables.js
├── modals.js
├── posts.js
├── notifications.js
├── notifications-center.js
├── account.js
├── dogs-modals.js
├── navbar-height.js
└── main.js
Arquitetura
22 de novembro de 2025
Single Page Application Lite
Arquitetura SPA-Lite: Uma Rota para Todo o Dashboard
O painel administrativo do sistema (GET /dashboard) funciona como uma Single Page Application (SPA) leve:
toda a interface — Gerenciar Usuários, Posts, Categorias, Galeria — vive em uma única rota. A troca entre seções é 100% client-side via JavaScript,
sem recarregamento de página.
🏗️ Como Funciona
Renderização Inicial
O endpoint GET /dashboard renderiza o orquestrador views/dashboard.twig (atualmente ~323 linhas), que inclui os partials com todas as seções no mesmo HTML final:
<div id="overview-section" class="content-section">
<div id="users-section" class="content-section" style="display:none;">
<div id="posts-section" class="content-section" style="display:none;">
<div id="gallery-list-section" class="content-section" style="display:none;">
- ...e assim por diante para todas as telas
Navegação Client-Side
O script public/js/dashboard/sidebar.js alterna seções e sincroniza hash amigável:
function showSection(sectionId, linkElement) {
document.querySelectorAll('.content-section')
.forEach(s => s.style.display = 'none');
const target = document.getElementById(sectionId + '-section');
if (target) target.style.display = 'block';
const hashKey = sectionIdToHash(sectionId); // ex.: users -> usuarios
const cleanUrl = window.location.pathname + window.location.search;
const nextUrl = sectionId === 'overview' ? cleanUrl : (cleanUrl + '#' + hashKey);
window.history.replaceState(null, '', nextUrl);
}
🔗 Fluxo Completo de Navegação
- Usuário acessa:
GET /dashboard → servidor renderiza dashboard.twig completo (overview visível, demais seções com display:none)
- Clique na sidebar: Link "👥 Usuários" tem
href="#users" + onclick="showSection('users', this)"
- JavaScript executa:
showSection('users') esconde todas as divs e exibe apenas #users-section
- Hash muda: URL vira
/dashboard#usuarios (mas sem requisição HTTP ao servidor)
- Estado preservado: Filtros de tabela, busca, scroll permanecem na memória enquanto você navega
- Ações POST: Criar/editar usuário →
POST /admin/users/create → redireciona para /dashboard#users (compatível legado) → JS detecta a seção e normaliza URL para /dashboard#usuarios
⚙️ Rotas Backend Auxiliares
| Tipo |
Rota |
Finalidade |
Redirect Padrão |
| GET |
/dashboard |
Renderiza template completo |
— |
| POST |
/admin/users/create |
Salva novo usuário |
/dashboard#users (normalizado para #usuarios) |
| POST |
/admin/posts/update |
Atualiza post existente |
/dashboard#posts (normalizado para #blog-posts) |
| POST |
/admin/gallery/delete |
Remove imagem da galeria |
/dashboard#gallery-list (normalizado para #galeria-imagens) |
| GET |
/notifications |
API JSON para bell dropdown |
— |
Resumo: É literalmente "uma rota para tudo" do ponto de vista de navegação visual,
com vários handlers POST/GET auxiliares para processar formulários e retornar para /dashboard#seção.
✅ Vantagens da Abordagem SPA-Lite
Performance e UX
- Menos requisições HTTP: Uma vez carregado
/dashboard, trocar entre Usuários → Posts → Galeria não recarrega CSS/JS
- Transições instantâneas: JS apenas esconde/mostra divs (
display:none), então a troca visual é <100ms vs. ~500ms+ de reload completo
- Estado preservado: Filtros de tabela, campos de busca, scroll position permanecem na memória durante navegação
- Cache browser: Scripts, imagens e estilos ficam em cache após o primeiro GET; rotas subsequentes só consomem markup mínimo
Simplicidade de Implementação
- Zero setup SPA: Não exige Webpack, Vite, npm build ou frameworks JS complexos
- Backend familiar: Continua sendo PHP/Twig; não precisa reescrever em React/Vue
- Debugging fácil: View Source mostra todo o HTML; inspecionar DOM é direto
- SEO não é problema: Dashboard é área autenticada; não precisa indexar Google
⚠️ Desvantagens e Trade-offs
Limitações Técnicas
- HTML inicial robusto: o dashboard inclui muitas seções no primeiro render. Mesmo com o orquestrador menor (~323 linhas), o HTML final continua amplo por carregar todos os partials
- Memória no browser: Todas as tabelas/modals ficam no DOM mesmo invisíveis. Com centenas de linhas pode pesar em dispositivos modestos
- Deep-linking limitado: O hash (
#users/#usuarios) não é rota real; se compartilhar /dashboard#posts, o backend não valida/redireciona caso sessão expire
Manutenibilidade
- Template monolítico: Um único arquivo dificulta refatoração; precisa dividir em partials Twig (
views/dashboard/sections/)
- Testes unitários JS: Módulos estão separados mas DOM ainda é acoplado; testar
showSection exige mock de elementos
- Escalabilidade: Se crescer para 50+ seções, o modelo atual não escala sem lazy-loading
🎯 É a Forma "Correta"?
Depende do Contexto
✅ Dashboards Pequenos/Médios
5–10 seções, <50 rotas: Perfeitamente válido e comum em CMSs.
WordPress admin usa abas hash, Laravel Nova tem abordagem similar.
Você ganha simplicidade sem setup complexo de SPA.
⚡ Apps Grandes (Escala)
Netflix admin, Shopify: SPA real (React/Next.js, Vue/Nuxt) com code-splitting.
Cada rota carrega só o JS/HTML necessário sob demanda.
Escala melhor mas exige bundlers, estado global (Redux/Pinia), APIs REST/GraphQL.
🔄 Híbridos Modernos
Inertia.js, Hotwire Turbo: Entrega SSR + transições SPA sem reescrever tudo em React.
É o "meio-termo profissional" atual — mantém backend PHP/Laravel com UX de SPA.
📊 Resumo da Arquitetura Atual
A abordagem é pragmática e eficiente para o escopo atual (dezenas de usuários/posts simultâneos).
Não é "errado" nem "amador" — é server-rendered SPA-lite, igual a muitos painéis admin PHP/Laravel modernos.
- Equilíbrio perfeito: Performance vs. complexidade sem over-engineering
- Pronto para crescer: Se precisar escalar (100+ seções, milhares de registros), migre para SPA modular com lazy-loading
- Manutenível: Com a refatoração recente (CSS/JS externos, helpers no controller), está preparado para dividir em partials
🔮 Próximas Evoluções Possíveis
| Melhoria |
Benefício |
Esforço |
Dividir dashboard.twig em partials |
Reduz complexidade do template principal |
Baixo |
| Lazy-load seções via AJAX |
Carrega HTML sob demanda, reduz payload inicial |
Médio |
| Migrar para Inertia.js |
Mantém PHP backend, ganha componentes Vue/React |
Médio |
| SPA completo (Next.js + API) |
Code-splitting automático, estado global robusto |
Alto |
Segurança
PHP
15 de novembro de 2025
Autenticação Segura com Bcrypt
Como protegemos senhas usando hashing bcrypt e validação de sessões.
Nunca armazene senhas em texto puro! No System, usamos as funções nativas do PHP
para hash seguro de senhas.
$hash = password_hash(
$_POST['password'],
PASSWORD_DEFAULT
);
// Gera: $2y$10$abc123...
$valid = password_verify(
$_POST['password'],
$user['password']
);
// Retorna true/false
O algoritmo bcrypt é projetado para ser computacionalmente caro, dificultando ataques
de força bruta. Cada hash é único mesmo para senhas idênticas (salt aleatório).
⚠️ Importante: Nunca use md5() ou sha1() para senhas!
Use sempre password_hash() e password_verify().
Banco de Dados
PDO
15 de novembro de 2025
Prepared Statements e SQL Injection
Por que usamos PDO com prepared statements e como isso previne SQL injection.
SQL injection é uma das vulnerabilidades mais comuns em aplicações web. Veja a diferença:
$sql = "SELECT * FROM users
WHERE email = '{$_POST['email']}'";
$result = $db->query($sql);
Vulnerável a: '; DROP TABLE users; --
$stmt = $db->prepare(
"SELECT * FROM users WHERE email = ?"
);
$stmt->execute([$_POST['email']]);
PDO escapa automático! Input malicioso é tratado como string.
Todos os métodos em app/Models/User.php usam prepared statements. Nunca concatenamos
input de usuário diretamente em queries SQL.
Templates
Twig
15 de novembro de 2025
Por Que Usar Twig ao Invés de PHP Puro?
Twig oferece segurança, legibilidade e separação de responsabilidades.
| Recurso |
PHP Puro |
Twig |
| Auto-escape HTML |
❌ Manual (htmlspecialchars()) |
✅ Automático |
| Herança de Templates |
⚠️ include() complexo |
✅ {% extends %} |
| Cache de Templates |
❌ Manual |
✅ Integrado |
| Sintaxe Limpa |
⚠️ |
✅ {{ variavel }} |
Esta página que você está lendo foi renderizada com Twig! Veja o código em views/docs-tech.twig.
Formulários
Validação
Atualizado em 19 de novembro de 2025
Formulário de Contato
Página: /contact — Formulário público com validação client/server, persistência no banco e layout consistente com outras páginas.
Estrutura Visual
- Wrapper:
.blog-page para largura consistente
- Header: Título + subtítulo + botões de navegação (Blog, Docs)
- Layout em 2 colunas:
- Coluna esquerda (col-lg-7): Formulário principal
- Coluna direita (col-lg-5): Outros canais (endereço, telefone, redes sociais, mapa)
- Footer callout: Links para Docs e About
Campos do Formulário
| Campo |
Tipo |
Validação |
name |
text |
required |
email |
email |
required, formato válido |
subject |
select |
required, opções pré-definidas no controller |
message |
textarea |
required, 5 linhas |
_token |
hidden |
CSRF validation |
Fluxo Backend
Rota POST em routes/web.php:
['path' => '/contact', 'method' => 'POST', 'handler' => 'HomeController@submitContact']
Controller HomeController::submitContact():
// 1. Valida CSRF token
// 2. Valida campos obrigatórios
// 3. Persiste no banco via ContactMessage::create()
// 4. Define flash message de sucesso/erro
// 5. Redireciona para /contact (PRG pattern)
Model ContactMessage:
// Tabela: contact_messages
// Campos: id, name, email, subject, message, status, created_at
// Método: create($data) insere via prepared statement
Validação Client-Side (Bootstrap)
JavaScript adiciona classe .was-validated no submit para feedback visual instantâneo:
<script>
var forms = document.querySelectorAll('.needs-validation');
Array.prototype.slice.call(forms).forEach(function(form) {
form.addEventListener('submit', function(event) {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
</script>
Flash Messages
Após o POST, mensagens são armazenadas na sessão e exibidas na próxima renderização:
{% set alert = flash_contact ?? null %}
{% if alert %}
<div class="alert alert-{{ alert.type }}">
{% for msg in alert.messages %}
<li>{{ msg }}</li>
{% endfor %}
</div>
{% endif %}
Outros Canais
A coluna lateral exibe informações complementares de contato:
- Endereço físico com nota sobre agendamento
- Telefone e WhatsApp com horário de atendimento
- E-mails diretos (suporte e comercial)
- Redes sociais (GitHub, LinkedIn, Twitter)
- Mapa Google Maps embarcado via iframe (ratio 16x9)
Responsividade Mobile
- Botões de ação empilham verticalmente em telas pequenas (
flex-column flex-sm-row)
- Botões ocupam largura total em mobile (
w-100 w-sm-auto)
- Layout de 2 colunas vira 1 coluna empilhada em
<992px
- Mapa mantém proporção 16:9 via
.ratio do Bootstrap
✓ Benefícios:
- Proteção CSRF contra ataques de falsificação
- Validação dupla (client + server) para melhor UX e segurança
- Persistência confiável com prepared statements (SQL injection prevention)
- Feedback imediato via flash messages
- Layout consistente com Blog, Gallery, About
- Múltiplos canais de contato para diferentes necessidades
UX
JavaScript
Atualizado em 19 de novembro de 2025
Botão "Voltar ao Topo"
Implementação: Botão flutuante que aparece após 300px de scroll, permitindo retorno suave ao topo da página.
Características
- Aparição condicional: Visível apenas após scroll > 300px
- Posição fixa: Canto inferior direito (bottom: 2rem, right: 2rem)
- Design: Botão quadrado com cantos arredondados (2.5rem × 2.5rem)
- Ícone: Bootstrap chevron-up (simples e minimalista)
- Animação suave: Fade in/out com transition 0.3s
- Scroll suave: behavior: 'smooth' no scrollTo
- Acessibilidade: aria-label e title descritivos
Implementação JavaScript
Criação dinâmica do botão:
const backToTopBtn = document.createElement('button');
backToTopBtn.innerHTML = '<i class="bi bi-chevron-up"></i>';
backToTopBtn.className = 'btn btn-primary position-fixed shadow';
backToTopBtn.style.cssText = 'bottom: 2rem; right: 2rem; width: 2.5rem; height: 2.5rem; border-radius: 0.375rem;';
document.body.appendChild(backToTopBtn);
Controle de visibilidade baseado em scroll:
window.addEventListener('scroll', function() {
if (window.pageYOffset > 300) {
backToTopBtn.style.opacity = '1';
backToTopBtn.style.visibility = 'visible';
} else {
backToTopBtn.style.opacity = '0';
backToTopBtn.style.visibility = 'hidden';
}
});
Scroll suave ao clicar:
backToTopBtn.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
Customização CSS
| Propriedade |
Valor |
Função |
position |
fixed |
Mantém botão visível durante scroll |
width / height |
2.5rem × 2.5rem |
Tamanho compacto similar ao navbar-toggler |
border-radius |
0.375rem |
Cantos arredondados (padrão Bootstrap) |
z-index |
1000 |
Garante sobreposição sobre outros elementos |
opacity |
0 → 1 |
Fade in/out suave |
visibility |
hidden → visible |
Remove do fluxo quando oculto |
transition |
all 0.3s ease |
Animação fluida de todas as mudanças |
Design Pattern
O botão segue o mesmo padrão visual do navbar-toggler do Bootstrap:
- Forma: Quadrado com cantos levemente arredondados
- Ícone:
bi-chevron-up (simples e minimalista)
- Cores:
btn-primary com shadow padrão
- Tamanho: Proporcional ao toggler (2.5rem vs 3rem)
Onde está implementado
- docs-tech.twig: Única página com implementação atual
- Razão: Conteúdo extenso (2700+ linhas) justifica a funcionalidade
- Gatilho: 300px de scroll vertical
- Posição: Bottom right (2rem de margem)
✓ Benefícios:
- Visual consistente com navbar-toggler do Bootstrap
- Melhora navegação em páginas longas
- Reduz esforço do usuário (sem scroll manual)
- Implementação leve (puro JavaScript vanilla)
- Sem dependências externas
- Responsivo e acessível
- Animação suave compatível com todos navegadores modernos
💡 Dica: Experimente rolar esta página para baixo e veja o botão aparecer no canto inferior direito com design similar ao menu mobile!
Novo
18 de novembro de 2025
Sistema de Permissões (RBAC)
Implementação completa de Role-Based Access Control com 4 níveis hierárquicos e workflow editorial.
Níveis de Acesso
| Role |
Badge |
Permissões |
| Subscriber |
Subscriber |
Acesso básico de visualização, sem permissões de criação |
| Author |
Author |
Criar posts com status draft ou pending_review, editar seus próprios posts |
| Editor |
Editor |
Publicar posts, gerenciar categorias, aprovar conteúdo pendente, ver todos os posts |
| Admin |
Admin |
Controle total: usuários, configurações do sistema, banco de dados, logs, rotas |
Workflow Editorial
📝 Processo de Publicação:
- Authors criam posts com status
draft (rascunho) ou pending_review (pendente)
- Editors visualizam queue de posts pendentes na seção Posts do dashboard
- Editors revisam e aprovam posts, alterando status para
published
- Authors veem apenas seus próprios posts no dashboard
- Editors/Admins veem todos os posts do sistema
Navegação Condicional
O dashboard adapta a sidebar baseado no role do usuário:
- Visão Geral: Todos os usuários autenticados
- Gerenciar Usuários: Apenas Admins
- Posts do Blog: Authors, Editors e Admins (Authors veem apenas seus posts)
- Categorias: Apenas Editors e Admins (dentro do dropdown de Posts)
- Sistema/Database/Logs/Rotas: Apenas Admins
Implementação Técnica
Database Schema
role ENUM('subscriber', 'author', 'editor', 'admin')
DEFAULT 'subscriber'
status ENUM('draft', 'pending_review', 'published')
DEFAULT 'draft'
Helper Methods (User Model)
// Verificar roles
User::isAdmin($user) // Apenas admin
User::isEditor($user) // Editor ou Admin
User::isAuthor($user) // Author, Editor ou Admin
// Verificar permissões específicas
User::canPublish($user) // Pode publicar diretamente
User::canEditPost($user, $post) // Pode editar post específico
User::canDeletePost($user, $post) // Pode deletar post específico
Funções Twig
{% if isAdmin(me) %}
<!-- Conteúdo apenas para admins -->
{% endif %}
{% if isEditor(me) %}
<!-- Conteúdo para editors e admins -->
{% endif %}
{% if isAuthor(me) %}
<!-- Conteúdo para authors, editors e admins -->
{% endif %}
{% if canPublish(me) %}
<option value="published">Publicado</option>
{% else %}
<option value="pending_review">Enviar para Revisão</option>
{% endif %}
✅ Benefícios do Sistema RBAC:
- Separação clara de responsabilidades entre equipe
- Workflow editorial profissional com aprovação de conteúdo
- Segurança aprimorada com least privilege principle
- Escalabilidade para equipes de qualquer tamanho
- Auditoria facilitada (cada usuário vê apenas o que pode acessar)
Atualização
16 de fevereiro de 2026
Matriz de Capacidades Centralizada
O dashboard agora usa uma matriz única para controlar seções e ações permitidas por perfil, evitando divergência entre backend, Twig e JavaScript.
Fonte Única de Verdade
app/Controllers/AdminController.php → método resolveDashboardPermissions()
- Retorno inclui
dashboard_capabilities.sections e dashboard_capabilities.actions
- As flags legadas (
can_manage_*, can_view_*) continuam expostas para compatibilidade gradual
Shape da matriz
'dashboard_capabilities' => [
'sections' => ['overview', 'users', 'dogs', 'account'],
'actions' => [
'manage_users' => false,
'view_users_list' => true,
'manage_dogs' => false,
'view_dogs_list' => true,
],
]
Fluxo de Consumo
- Backend: calcula capacidades por usuário autenticado
- Twig: injeta matriz em
window.dashboardConfig.capabilities no views/dashboard.twig
- Frontend:
public/js/dashboard/main.js usa capabilities.sections para whitelist de seções/hash
- Segurança: endpoints críticos também validam permissão no controller (nunca confiar só na UI)
Padrão para Novos Módulos
Sempre aplicar em duas camadas:
- Camada de navegação/render: sidebar, seções e botões
- Camada de endpoint: middleware + validação no método do controller
Exemplo aplicado: módulo de cães em modo leitura para subscriber com lista visível, sem ações de gestão (editar/exportar/excluir).
Novo
18 de novembro de 2025
Workflow Editorial Completo
Entenda o fluxo de criação, revisão e publicação de posts com o sistema de aprovação hierárquico.
Conceito Fundamental:
O workflow editorial garante que Authors não possam publicar diretamente.
Todo conteúdo passa por aprovação de um Editor ou Admin antes de ir ao ar,
garantindo qualidade e controle editorial.
🎯 Status de Posts Explicados
| Status |
Badge |
Significado |
Quem Vê |
| draft |
Rascunho |
Post em elaboração - NÃO MEXER |
Apenas o Author que criou |
| pending_review |
Pendente |
Post PRONTO aguardando aprovação |
Author criador + Editors/Admins |
| published |
Publicado |
Post aprovado e visível publicamente |
Todos os visitantes do site |
🔄 Fluxo Completo (Passo a Passo)
Fase 1 Criação (Author)
- Author acessa Dashboard → Posts do Blog → Novo Post
- Preenche título, conteúdo, categoria, tags, etc.
- Define status como
draft (rascunho)
- Clica em "Salvar Post"
Resultado: Post criado com author_id do usuário logado.
Visível apenas para o Author na lista de posts.
Fase 2 Edição Livre (Author)
- Enquanto estiver em
draft, Author pode editar livremente
- Fazer ajustes no texto, adicionar imagens, alterar categoria
- Salvar quantas vezes necessário
Proteção: Editors/Admins veem que o post existe,
mas não devem editar posts em draft - significa que o Author ainda está trabalhando.
Fase 3 Submissão para Revisão (Author)
- Author termina o conteúdo
- Revisa título, ortografia, formatação
- Edita o post e altera status para
pending_review
- Salva o post
Resultado: Post entra na fila de revisão.
Editors/Admins veem alerta: "Posts Aguardando Revisão: X"
⚠️ Importante: Author NÃO deve editar posts em pending_review.
Aguarde feedback do Editor antes de fazer alterações.
Fase 4 Revisão e Aprovação (Editor/Admin)
- Editor acessa Dashboard → Posts do Blog
- Vê alerta de posts pendentes
- Abre o post em
pending_review
- Revisa conteúdo, gramática, adequação ao estilo editorial
- Faz ajustes se necessário (pode editar diretamente)
-
Opção A - Aprovar: Altera status para
published → Post vai ao ar
Opção B - Recusar: Volta status para draft + envia feedback ao Author
Resultado: Se aprovado, post fica visível em /blog para todos os visitantes.
Fase 5 Pós-Publicação
- Post publicado pode ser editado por Editors/Admins a qualquer momento
- Authors podem sugerir edições criando um novo draft ou enviando feedback
- Editor pode despublicar voltando status para
draft
📊 Resumo de Permissões por Role
Pode:
- Criar novos posts
- Editar seus próprios posts em
draft
- Enviar posts para revisão (
pending_review)
- Ver apenas seus posts na lista
- Categorizar e adicionar tags
NÃO Pode:
- Publicar diretamente (
published)
- Editar posts de outros Authors
- Ver posts de outros Authors
- Alterar o autor de um post
Pode:
- Ver todos os posts do sistema
- Editar qualquer post (inclusive de outros)
- Publicar posts diretamente (
published)
- Aprovar ou recusar posts pendentes
- Alterar o autor de um post
- Gerenciar categorias e tags
- Despublicar posts
Responsabilidade:
- Revisar fila de posts pendentes regularmente
- Dar feedback construtivo aos Authors
- Garantir qualidade editorial
💡 Boas Práticas Recomendadas
Para Authors:
- ✅ Use
draft enquanto estiver escrevendo e revisando
- ✅ Só mude para
pending_review quando o post estiver 100% pronto
- ✅ Revise ortografia, gramática e formatação antes de enviar
- ✅ Adicione categoria e tags relevantes
- ✅ Inclua uma imagem de capa atraente
- ❌ Não edite posts em
pending_review - aguarde feedback
- ❌ Não envie posts incompletos para revisão
Para Editors:
- ✅ Revise a fila de posts pendentes pelo menos 1x por dia
- ✅ Dê feedback construtivo quando recusar um post
- ✅ Respeite o estilo do Author, faça apenas ajustes necessários
- ✅ Verifique SEO (título, excerpt, tags) antes de publicar
- ✅ Se recusar um post, explique o motivo ao Author
- ❌ Não edite posts em
draft sem avisar o Author
- ❌ Não publique posts com erros ou conteúdo inadequado
🔍 Exemplo Prático Completo
Cenário: João (Author) quer publicar um artigo sobre PHP 8.3
-
Dia 1 - Criação:
João cria o post "Novidades do PHP 8.3", status draft.
Escreve 500 palavras, adiciona código, salva.
-
Dia 2 - Edição:
João adiciona mais 1000 palavras, imagens, exemplos práticos.
Post ainda em draft, visível só para ele.
-
Dia 3 - Finalização:
João revisa o texto, adiciona categoria "PHP", tags "php8, novidades".
Altera status para pending_review, salva.
-
Dia 3 (tarde) - Revisão:
Maria (Editor) vê alerta "Posts Aguardando Revisão: 1".
Abre o post de João, lê atentamente, corrige 3 erros de digitação.
-
Dia 3 (noite) - Aprovação:
Maria altera status para published, define data de publicação como "agora".
Post vai ao ar em /blog/novidades-do-php-83.
-
Resultado:
Post publicado com sucesso! João vê seu artigo no site, recebe crédito como autor.
Maria garantiu qualidade editorial antes da publicação.
✅ Por que esse workflow é importante:
- Qualidade: Todo conteúdo passa por revisão profissional
- Consistência: Mantém padrão editorial em todos os posts
- Proteção: Evita publicação acidental de rascunhos incompletos
- Responsabilidade: Clara separação entre criação e aprovação
- Rastreabilidade: Histórico completo de quem criou e quem aprovou
Novo
18 de novembro de 2025
Sistema de Notificações em Tempo Real
Sistema completo de notificações integrado ao workflow editorial, com AJAX, auto-refresh e interface responsiva.
Funcionalidade Principal:
O sistema notifica automaticamente Editors quando Authors enviam posts para revisão e
Authors quando seus posts são aprovados ou rejeitados, garantindo comunicação eficiente
entre a equipe editorial.
🎯 Gatilhos de Notificação Automática
| Evento |
Tipo |
Destinatário |
Mensagem |
| Author envia post para revisão |
post_pending |
Todos os Editors/Admins |
"[Author] enviou um post para revisão" |
| Editor aprova e publica post |
post_published |
Author do post |
"Seu post '[Título]' foi publicado!" |
| Editor rejeita post (volta para draft) |
post_returned |
Author do post |
"Seu post '[Título]' foi devolvido para revisão" |
| Admin cria/edita/remove usuário |
user_created, user_updated, user_deleted |
Admins |
"[Admin] atualizou o perfil de [Usuário]" |
| Fluxo de aprovação de usuário |
user_approved, user_pending_approval |
Usuário alvo + Admins |
"Conta aprovada" / "Cadastro aguardando aprovação" |
| Admin cria/edita/remove cão |
dog_created, dog_updated, dog_deleted |
Admins |
"[Admin] atualizou os dados de [Cão]" |
⚙️ Arquitetura Backend
📁 Banco de Dados
-- Tabela notifications
CREATE TABLE IF NOT EXISTS notifications (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT,
link VARCHAR(255),
is_read TINYINT(1) DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_read (user_id, is_read),
INDEX idx_created (created_at DESC)
);
Índices otimizados: (user_id, is_read) para buscar não lidas rapidamente,
created_at DESC para ordenação temporal eficiente.
📦 Model: Notification.php
Métodos principais:
create($userId, $type, $title, $message, $link) - Cria nova notificação
getUnreadCount($userId) - Retorna quantidade de não lidas
getRecent($userId, $limit) - Busca últimas N notificações
markAsRead($id, $userId) - Marca individual como lida
markAllAsRead($userId) - Marca todas como lidas
notifyEditors($title, $message, $link) - Notifica todos Editors/Admins
notifyUser($userId, $type, $title, $message, $link) - Notifica usuário específico
🛣️ Rotas API
GET /notifications - Retorna JSON com notificações + contador de não lidas
POST /notifications/mark-read/{id} - Marca notificação específica como lida
POST /notifications/mark-all-read - Marca todas como lidas em batch
Todas as rotas protegidas com middleware 'auth' - apenas usuários logados.
🎬 Integração com PostsAdminController
Gatilhos automáticos no método postsUpdate():
// Quando status muda de draft → pending_review
if ($oldStatus === 'draft' && $newStatus === 'pending_review') {
Notification::notifyEditors(
'Novo post aguardando revisão',
"{$author['name']} enviou um post para revisão",
'/dashboard#posts'
);
}
// Quando post é publicado por Editor/Admin
if ($newStatus === 'published' && $oldStatus !== 'published') {
Notification::notifyUser(
$authorId,
'post_published',
'Post publicado com sucesso!',
"Seu post '{$title}' está no ar!",
"/blog/{$slug}"
);
}
// Quando post é rejeitado (volta para draft)
if ($oldStatus === 'pending_review' && $newStatus === 'draft') {
Notification::notifyUser(
$authorId,
'post_returned',
'Post devolvido para revisão',
"Seu post '{$title}' precisa de ajustes",
'/dashboard#posts-edit-' . $postId
);
}
Os links acima usam hashes legados internos (#posts, #posts-edit-*).
No frontend atual, o dashboard aceita esses hashes e normaliza para os aliases visíveis na URL
(ex.: #blog-posts).
🎨 Interface Frontend
🔔 Dropdown de Notificações
Componentes visuais:
- Sino animado: Ícone Bootstrap
bi-bell-fill com efeito hover
- Badge dinâmico: Contador vermelho sobreposto (oculto quando 0)
- Dropdown responsivo: 350px de largura em desktop, 100vw em mobile
- Altura máxima: 500px com scroll automático
- Header fixo: "Notificações" + botão "Marcar todas como lidas"
- Lista scrollável: Container
#notificationsList com itens dinâmicos
Ajustes para mobile: Dropdown ocupa largura total da tela (min-width: 100vw),
alinhado à direita para facilitar acesso com polegar.
⚡ JavaScript: Gerenciamento de Notificações
Funções implementadas:
-
loadNotifications() - Carrega via AJAX (GET /notifications)
- Chama
renderNotifications() e updateBadge()
-
renderNotifications(notifications) - Renderiza lista HTML dinâmica
- Mostra ícones coloridos por tipo (
getNotificationIcon())
- Formata timestamp relativo (
formatTimeAgo())
- Destaca não lidas com fundo claro e dot azul
- Adiciona links clicáveis quando disponíveis
-
getNotificationIcon(type) - Retorna ícone Bootstrap específico
post_pending → bi-clock-history (amarelo)
post_published → bi-check-circle (verde)
post_returned → bi-arrow-return-left (azul)
user_* → ícones de usuário (criação, aprovação, atualização, remoção)
dog_* → ícones de gestão de cães (cadastro, atualização, remoção)
-
formatTimeAgo(timestamp) - Formata em português
- "Agora mesmo" (< 1min) → "5 min atrás" → "2h atrás" → "3d atrás"
-
updateBadge(count) - Atualiza contador dinâmico
- Mostra número exato até 99, depois "99+"
- Oculta badge quando count = 0
-
markNotificationRead(id) - Marca individual via POST
- Recarrega lista após 300ms para feedback visual
-
markAllNotificationsRead() - Marca todas via POST
- Atualiza badge e lista imediatamente
🔄 Auto-Refresh e Eventos
Carregamento inteligente:
- Na inicialização:
loadNotifications() ao carregar página
- Ao abrir dropdown: Recarrega automaticamente (evento
click)
- Auto-refresh: Atualiza a cada 30 segundos (
setInterval(loadNotifications, 30000))
- Após marcar lida: Recarrega com delay de 300ms para animação suave
📱 Responsividade Mobile
Ajustes para telas pequenas:
@media (max-width: 576px) {
.dropdown-menu.notification-dropdown {
min-width: 100vw !important;
max-width: 100vw !important;
left: auto !important;
right: 0 !important;
margin-right: -1rem;
}
.dropdown-menu.notification-dropdown .notification-item {
padding: 1rem 0.75rem !important;
}
.notification-badge {
font-size: 0.65rem !important;
padding: 0.2rem 0.35rem !important;
}
}
Otimizações mobile:
- Dropdown ocupa largura total (100vw) para facilitar leitura
- Padding reduzido nos itens para mais conteúdo visível
- Badge menor para não obstruir ícone do sino
- Touch-friendly: áreas clicáveis amplas (min 44x44px)
🔍 Exemplo de Fluxo Completo
Cenário: João (Author) envia post, Maria (Editor) aprova
-
T+0s - João envia post:
João clica em "Salvar Post" com status pending_review.
PostsAdminController::postsUpdate() detecta mudança de status.
Notification::notifyEditors() cria notificações para todos Editors.
-
T+5s - Maria recebe notificação:
Sistema auto-refresh (30s) ou Maria clica no sino.
AJAX busca notificações, renderiza item com ícone amarelo.
Badge mostra "1" em vermelho.
-
T+10s - Maria clica na notificação:
markNotificationRead(id) envia POST.
Notificação marcada como lida no banco.
Maria é redirecionada para /dashboard#posts (em seguida normalizado para /dashboard#blog-posts).
-
T+15s - Maria aprova post:
Maria edita post, altera status para published.
PostsAdminController::postsUpdate() detecta aprovação.
Notification::notifyUser() cria notificação para João.
-
T+20s - João recebe confirmação:
João vê badge "1", abre dropdown.
Notificação verde: "Seu post 'Título' foi publicado!"
Clica, vai para /blog/titulo-do-post.
✅ Benefícios do Sistema de Notificações:
- Comunicação instantânea: Equipe sempre atualizada sobre status dos posts
- Redução de e-mails: Notificações in-app substituem alertas por e-mail
- UX aprimorada: Feedback visual imediato com ícones e cores
- Performance: Auto-refresh em 30s evita sobrecarga no servidor
- Responsivo: Interface adaptada para desktop, tablet e mobile
- Escalável: Fácil adicionar novos tipos de notificação (comentários, menções, etc.)
Próximas Melhorias Sugeridas:
- Adicionar notificação sonora ao receber nova notificação
- Implementar WebSockets para push real-time (sem polling)
- Criar página dedicada "Ver todas as notificações" com paginação
- Notificar Admins sobre novos registros de usuários
- Permitir usuários configurarem preferências de notificação
Novo
19 de novembro de 2025
Blog com Busca Backend e API JSON
Sistema de blog com busca/ordenação server-side, layout horizontal responsivo e endpoints JSON para consumo via API.
Funcionalidade Principal:
O blog possui busca e ordenação backend (evitando manipulação client-side),
layout horizontal full-width para melhor escaneabilidade, e
botões JSON que expõem dados estruturados via API REST.
🔍 Busca e Ordenação Backend
📊 Parâmetros GET Suportados
| Parâmetro |
Valores |
Função |
Exemplo |
q |
String |
Busca em título, excerpt, conteúdo e categoria |
/blog?q=php |
filter |
'' (padrão)
oldest
az
za |
Ordenação dos posts |
/blog?filter=az |
page |
Integer |
Paginação (10 posts/página) |
/blog?page=2 |
⚙️ Implementação em HomeController::blog()
// 1. Busca via mb_stripos (case-insensitive, multibyte-safe)
$searchTerm = $_GET['q'] ?? '';
if ($searchTerm) {
$posts = array_filter($posts, function($post) use ($searchTerm) {
return mb_stripos($post['title'], $searchTerm) !== false
|| mb_stripos($post['excerpt'], $searchTerm) !== false
|| mb_stripos($post['content'], $searchTerm) !== false
|| mb_stripos($post['category_name'], $searchTerm) !== false;
});
}
// 2. Ordenação via usort
$filter = $_GET['filter'] ?? '';
switch ($filter) {
case 'oldest':
usort($posts, fn($a, $b) =>
strtotime($a['published_at']) <=> strtotime($b['published_at'])
);
break;
case 'az':
usort($posts, fn($a, $b) => strcasecmp($a['title'], $b['title']));
break;
case 'za':
usort($posts, fn($a, $b) => strcasecmp($b['title'], $a['title']));
break;
default: // 'recent'
usort($posts, fn($a, $b) =>
strtotime($b['published_at']) <=> strtotime($a['published_at'])
);
}
// 3. Paginação (10 posts/página)
$perPage = 10;
$totalPosts = count($posts);
$totalPages = ceil($totalPosts / $perPage);
$currentPage = max(1, min($totalPages ?: 1, $_GET['page'] ?? 1));
$posts = array_slice($posts, ($currentPage - 1) * $perPage, $perPage);
✅ Vantagens da Busca Backend
- SEO-friendly: URLs compartilháveis e indexáveis (
/blog?q=php&filter=az)
- Performance: Carrega apenas 10 posts por vez (não todo o dataset)
- Segurança: Validação server-side, sem manipulação DOM no cliente
- Compatibilidade: Funciona sem JavaScript habilitado
- Estado preservado: Parâmetros mantidos na paginação
🎨 Layout Horizontal Full-Width
🖼️ Estrutura de Card
<article class="card sys-surface shadow-sm mb-4">
<div class="row g-0">
<!-- Imagem à esquerda (col-md-4) -->
<div class="col-md-4">
<div class="blog-card-cover-wrapper h-100">
<img src="" class="blog-card-cover h-100 w-100">
</div>
</div>
<!-- Conteúdo à direita (col-md-8) -->
<div class="col-md-8">
<div class="card-body d-flex flex-column h-100">
<!-- Metadados: categoria, data, tempo leitura, autor -->
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
<a href="/blog/category/"
class="badge"
style="background-color: ">
</a>
<small>📅 09/04/2026</small>
<small>⏱️ min</small>
<small>👤 <a href="/blog/author/">
</a></small>
</div>
<!-- Título -->
<h3 class="h4 mb-2">
<a href="/blog/"></a>
</h3>
<!-- Resumo truncado (200 chars) -->
<p class="text-muted mb-3">
</p>
<!-- Botões (alinhados ao fim com mt-auto) -->
<div class="mt-auto d-flex flex-wrap gap-2">
<a href="/blog/" class="btn btn-sm btn-primary">
📖 Ler post completo
</a>
<a href="/api/posts/"
class="btn btn-sm btn-outline-secondary"
target="_blank">
{} JSON
</a>
</div>
</div>
</div>
</div>
</article>
✅ Benefícios do Layout Horizontal
- Escaneabilidade: Padrão F-pattern facilita leitura rápida de títulos e resumos
- Densidade de informação: Mais metadados visíveis sem scroll
- Responsivo: Colapsa para vertical (col-12) em mobile
- Acessibilidade: Estrutura semântica com
<article> e headings adequados
- Consistência: Imagens sempre proporcionais (object-fit: cover)
🔗 Botão JSON e API REST
🎯 Finalidade do Endpoint JSON
Casos de uso principais:
-
Consumo por APIs externas:
- Aplicativos móveis podem buscar posts via GET
/api/posts/{slug}
- Integrações com plataformas de terceiros (Medium, Dev.to, etc.)
- Agregadores de conteúdo RSS/JSON
-
Depuração e desenvolvimento:
- Desenvolvedores visualizam estrutura completa dos dados
- Teste rápido de campos, tipos e relacionamentos
- Validação de dados antes de renderizar templates
-
Integrações headless CMS:
- Frontend React/Vue/Angular pode consumir JSON puro
- SSG (Static Site Generators) como Next.js, Gatsby
- Implementação de PWA com cache offline
-
SEO e dados estruturados:
- Base para implementar JSON-LD (Schema.org)
- Rich snippets para Google Search
- Open Graph tags dinâmicas
-
Exportação e backup:
- Usuários avançados podem copiar/exportar dados brutos
- Migração de conteúdo para outras plataformas
- Arquivamento em formato estruturado
📋 Estrutura de Resposta JSON Esperada
{
"id": 1,
"title": "Arquitetura MVC em PHP Moderno",
"slug": "arquitetura-mvc-php-moderno",
"excerpt": "Entenda o padrão Model-View-Controller...",
"content": "<p>Conteúdo HTML completo do post...</p>",
"cover": "/uploads/posts/mvc-cover.jpg",
"status": "published",
"published_at": "2025-11-15T10:30:00Z",
"created_at": "2025-11-10T14:20:00Z",
"updated_at": "2025-11-15T10:30:00Z",
"reading_time": 8,
"views": 1547,
"category": {
"id": 3,
"name": "PHP",
"slug": "php",
"color": "#777bb4",
"description": "Artigos sobre PHP e frameworks"
},
"author": {
"id": 5,
"name": "João Silva",
"email": "joao@example.com",
"role": "author",
"avatar": "/uploads/avatars/joao.jpg"
},
"tags": ["php", "mvc", "arquitetura", "boas-praticas"],
"meta": {
"seo_title": "MVC em PHP: Guia Completo",
"seo_description": "Aprenda MVC com exemplos práticos",
"og_image": "/uploads/posts/mvc-og.jpg"
}
}
Nota: A estrutura exata depende da implementação do endpoint
/api/posts/{slug} no HomeController ou ApiController.
🔐 Considerações de Segurança
- CORS: Configurar headers adequados se permitir consumo cross-origin
- Rate limiting: Limitar requisições por IP para evitar abuso
- Filtro de status: API deve retornar apenas posts
published para público
- Sanitização: Remover dados sensíveis (IPs, drafts, emails privados)
- Cache: Implementar cache HTTP (ETag, Last-Modified) para performance
🎯 Formulário de Busca e UX
🔍 Componentes do Formulário
<form class="row g-3 mb-4" method="get" action="/blog">
<!-- Campo de busca (col-md-8) -->
<div class="col-md-8">
<label for="blogSearch" class="form-label small text-uppercase">
Buscar posts
</label>
<div class="input-group">
<span class="input-group-text">🔍</span>
<input type="text"
class="form-control"
id="blogSearch"
name="q"
placeholder="Título, conteúdo, categoria..."
value="">
</div>
</div>
<!-- Select de ordenação (col-md-4) -->
<div class="col-md-4">
<label for="blogFilter" class="form-label small text-uppercase">
Ordenar
</label>
<select class="form-select"
id="blogFilter"
name="filter"
onchange="this.form.submit()">
<option value="" selected>
Mais recentes
</option>
<option value="oldest" >
Mais antigos
</option>
<option value="az" >
A-Z (título)
</option>
<option value="za" >
Z-A (título)
</option>
</select>
</div>
</form>
<!-- Alert de resultados -->
✅ Melhorias de UX Implementadas
- Botão "Limpar": Aparece apenas quando há busca ativa, reseta filtros
- Auto-submit: Select de ordenação submete form automaticamente (
onchange)
- Preservação de estado: Valores preenchidos mantidos após submit
- Feedback visual: Alert mostra termo buscado e quantidade de resultados
- Placeholder descritivo: "Título, conteúdo, categoria..." orienta usuário
- Responsivo: col-md-8/4 em desktop, col-12 em mobile
📄 Paginação com Preservação de Parâmetros
🔗 Geração Dinâmica de Links
<!-- Exemplo de uso na paginação -->
<a class="page-link" href="/blog?page=">
</a>
<!-- URL gerada: /blog?page=2&q=php&filter=az -->
✅ Vantagens da Preservação
- Experiência fluida: Usuário não perde filtros ao navegar páginas
- Bookmarkable: URLs completas podem ser salvas/compartilhadas
- Navegação do browser: Botões voltar/avançar funcionam corretamente
- Analytics: Google Analytics rastreia busca + paginação juntos
✅ Benefícios Gerais do Sistema de Blog:
- Performance: Paginação reduz carga, busca backend evita JS desnecessário
- SEO: URLs semânticas, conteúdo renderizado server-side, meta tags adequadas
- API-first: Botões JSON permitem headless CMS e integrações externas
- Acessibilidade: Form funciona sem JS, estrutura semântica (article, nav)
- Responsividade: Layout horizontal adapta para vertical em mobile
- Manutenibilidade: Lógica de busca/ordenação centralizada no Controller
Próximas Melhorias Sugeridas:
- Adicionar filtros por categoria e autor (checkboxes/multi-select)
- Implementar busca full-text com MySQL MATCH...AGAINST
- Cache de resultados de busca com Redis/Memcached
- Sugestões de busca (autocomplete) via AJAX
- Estatísticas de busca (termos mais buscados, zero results)
- API GraphQL como alternativa ao REST JSON
Novo
19 de novembro de 2025
Artigo em Destaque no Blog
Sistema de destaque visual para o post mais recente, com card expandido e maior visibilidade na página inicial do blog.
Funcionalidade Principal:
O primeiro post publicado aparece em destaque na página 1 do blog (sem busca ativa),
com layout expandido, imagem maior e conteúdo destacado para atrair atenção do leitor.
🎯 Lógica de Exibição
📋 Condições para Exibir Featured Post
- Apenas na página 1: Não aparece em páginas 2, 3, etc.
- Sem busca ativa: Se houver termo de busca, featured é ocultado
- Post mais recente: Primeiro item do array ordenado por
published_at DESC
- Removido da lista normal: Usa
array_shift() para evitar duplicação
⚙️ Implementação Backend
public function blog()
{
$postModel = new Post($this->config);
$allPosts = $postModel->allPublished();
// Extrai post em destaque (primeiro) apenas na página 1 e sem busca
$featuredPost = null;
$searchTerm = trim($_GET['q'] ?? '');
$page = max(1, (int)($_GET['page'] ?? 1));
if ($page === 1 && $searchTerm === '' && !empty($allPosts)) {
$featuredPost = array_shift($allPosts); // Remove primeiro post
}
// ... restante da lógica de busca e paginação
// Paginação ajustada: 9 posts normais + 1 featured = 10 total
$perPage = 9;
$this->render('blog.twig', [
'featured_post' => $featuredPost,
'posts' => $posts,
// ...
]);
}
🎨 Design e Layout
🖼️ Estrutura Visual do Featured Post
Componentes:
- Badge "Artigo em Destaque":
bg-warning text-dark com ícone bi-star-fill
- Card grande:
shadow-lg (vs shadow-sm dos normais)
- Layout 50/50:
col-lg-6 imagem + col-lg-6 conteúdo
- Imagem ampliada:
min-height: 400px (vs 250px dos cards normais)
- Título maior:
h2 (vs h4 nos posts normais)
- Excerpt expandido: 300 caracteres (vs 200 dos normais)
- Badge de categoria maior:
fs-6 para destaque
- Botões padrão:
btn (vs btn-sm nos cards pequenos)
🎭 Estrutura HTML
<section class="featured-post mb-5">
<div class="badge bg-warning text-dark mb-3">
⭐ Artigo em Destaque
</div>
<article class="card sys-surface shadow-lg border-0">
<div class="row g-0">
<!-- Imagem (col-lg-6) -->
<div class="col-lg-6">
<div class="featured-cover-wrapper h-100">
<img src=""
class="featured-cover h-100 w-100">
</div>
</div>
<!-- Conteúdo (col-lg-6) -->
<div class="col-lg-6">
<div class="card-body d-flex flex-column h-100 p-4">
<!-- Metadados -->
<div class="d-flex gap-2 mb-3">
<a class="badge fs-6"
style="background-color: ">
</a>
<small>📅 09/04/2026</small>
<small>⏱️ min</small>
</div>
<!-- Título H2 -->
<h2 class="h2 mb-3">
<a href="/blog/"></a>
</h2>
<!-- Excerpt expandido (300 chars) -->
<p class="lead text-muted mb-4">
...
</p>
<!-- Autor -->
<div class="d-flex align-items-center gap-2 mb-4">
👤 Por <a href="/blog/author/">
</a>
</div>
<!-- Botões -->
<div class="mt-auto d-flex gap-2">
<a href="/blog/" class="btn btn-primary">
📖 Ler artigo completo
</a>
<a href="/api/posts/"
class="btn btn-outline-secondary">
{} JSON
</a>
</div>
</div>
</div>
</div>
</article>
</section>
<hr class="my-5">
<h3 class="h5 text-muted text-uppercase fw-semibold mb-4">
📋 Outros Artigos
</h3>
✨ Efeitos Visuais e Interatividade
🎬 Animações CSS
/* Card hover effect */
.featured-post .card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.featured-post .card:hover {
transform: translateY(-5px);
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
}
/* Image zoom on hover */
.featured-cover {
object-fit: cover;
transition: transform 0.5s ease;
}
.featured-post .card:hover .featured-cover {
transform: scale(1.05);
}
/* Gradient placeholder when no cover */
.featured-cover-wrapper {
min-height: 400px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.featured-cover-wrapper.blog-card-cover-placeholder {
background: linear-gradient(135deg,
rgba(var(--bs-primary-rgb), 0.1) 0%,
rgba(var(--bs-secondary-rgb), 0.1) 100%);
}
📱 Responsividade
- Desktop (≥992px): Layout horizontal 50/50 (col-lg-6 + col-lg-6)
- Tablet (768-991px): Imagem no topo, conteúdo embaixo (altura 300px)
- Mobile (≤767px): Stack vertical completo (altura 250px), título h2 → 1.5rem
@media (max-width: 991px) {
.featured-cover-wrapper {
min-height: 300px;
}
.featured-post .card-body {
padding: 2rem !important;
}
}
@media (max-width: 576px) {
.featured-cover-wrapper {
min-height: 250px;
}
.featured-post h2 {
font-size: 1.5rem;
}
}
🌗 Suporte a Dark Theme
[data-theme='dark'] .featured-post .card {
background: rgba(255, 255, 255, 0.05) !important;
}
[data-theme='dark'] .featured-post .card:hover {
background: rgba(255, 255, 255, 0.08) !important;
}
O card em destaque recebe fundo semi-transparente no tema escuro,
ficando mais brilhante no hover para feedback visual.
🔄 Fluxo de Usuário
Cenário: Visitante acessa o blog
-
Landing inicial: Visitante acessa
/blog (página 1, sem busca)
Sistema exibe badge "Artigo em Destaque" + card grande do post mais recente
-
Scroll down: Após o featured, vê separador HR + heading "Outros Artigos"
Lista horizontal mostra 9 posts seguintes (total 10 com o featured)
-
Busca ativa: Visitante digita termo no campo "Buscar posts"
Featured desaparece, mostra apenas resultados filtrados (evita confusão)
-
Ordenação diferente: Visitante seleciona "Mais antigos" ou "A-Z"
Featured continua visível (ainda página 1), mas mostra o primeiro após ordenação
-
Página 2+: Visitante clica em paginação
Featured desaparece, mostra apenas os 9 posts da página atual
📊 Impacto na Paginação
| Situação |
Posts por Página |
Featured Visível? |
Total Exibido |
| Página 1, sem busca |
9 normais |
✅ Sim |
10 posts (1 featured + 9 normais) |
| Página 2+, sem busca |
9 normais |
❌ Não |
9 posts |
| Qualquer página com busca |
9 normais |
❌ Não |
9 posts filtrados |
✅ Benefícios do Featured Post:
- Hierarquia visual clara: Conteúdo mais importante em destaque
- Maior CTR: Card grande com imagem atraente aumenta cliques
- SEO interno: Posts recentes ganham visibilidade imediata
- UX aprimorada: Visitantes identificam rapidamente o último artigo
- Responsivo: Adapta layout para mobile sem perder impacto
- Performance: Não afeta paginação (apenas remove 1 post da lista)
Próximas Melhorias Sugeridas:
- Permitir admin marcar manualmente qual post deve ser featured (flag
is_featured no DB)
- Adicionar campo "featured_until" para controlar duração do destaque
- Implementar carrossel de múltiplos featured posts (2-3 rotacionando)
- A/B testing: medir impacto do featured vs lista uniforme
- Meta tag Open Graph específica para featured (preview social aprimorado)
Novo
19 de novembro de 2025
Galeria Visual Interativa
Galeria de imagens com filtros por categoria, lightbox modal e grid responsivo para showcase de inspirações visuais do projeto.
Funcionalidade Principal:
Apresenta imagens do Unsplash que inspiraram o design do sistema, organizadas por categorias
(Infraestrutura, Backend, UI, Dev Experience, Segurança) com filtros client-side e
visualização ampliada via Bootstrap Modal.
🎯 Características Principais
📋 Estrutura Visual
- Mesma estrutura do Blog: Classe
.blog-page para largura consistente
- Header padronizado: Título + subtítulo + botões (Blog, Docs)
- Grid responsivo: 3 colunas (desktop) → 2 (tablet) → 1 (mobile)
- 8 imagens curadas: Alta resolução do Unsplash (1200px thumbs, 1920px lightbox)
- Footer callout: CTA para Documentação e Blog
🎨 Cards de Imagem
Estrutura de cada card:
<article class="card sys-surface shadow-sm h-100 gallery-card">
<!-- Imagem com overlay -->
<div class="gallery-image-wrapper"
style="background-image:url(...)"
onclick="openLightbox(...)">
<div class="gallery-overlay">
<i class="bi bi-zoom-in"></i>
</div>
</div>
<!-- Conteúdo -->
<div class="card-body">
<span class="badge bg-primary mb-2">Categoria</span>
<h3 class="h5 mb-2">Título da Imagem</h3>
<p class="text-muted small mb-0">Descrição breve</p>
</div>
</article>
🔍 Sistema de Filtros
Filtros implementados:
Todas - Mostra todas as 8 imagens (padrão)
Infraestrutura - Clusters, cloud, servidores
Backend - Code review, colaboração
UI & Dashboard - Interfaces, painéis, dark mode
Dev Experience - Setups, workstations, quadros
Segurança - Monitoramento, observabilidade
Funcionamento: JavaScript client-side filtra via data-category sem reload da página.
⚡ JavaScript de Filtros
// Filtros de categoria
document.querySelectorAll('[data-filter]').forEach(btn => {
btn.addEventListener('click', function() {
const filter = this.dataset.filter;
// Atualiza botões ativos
document.querySelectorAll('[data-filter]').forEach(b =>
b.classList.remove('active')
);
this.classList.add('active');
// Filtra items
document.querySelectorAll('.gallery-item').forEach(item => {
if (filter === 'all' || item.dataset.category === filter) {
item.classList.remove('hidden');
} else {
item.classList.add('hidden');
}
});
});
});
Performance: Filtro instant (sem AJAX), transições CSS suaves (opacity + scale).
🖼️ Lightbox Modal
Visualização ampliada:
<!-- Modal Bootstrap -->
<div class="modal fade" id="lightboxModal">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content bg-dark">
<div class="modal-header border-0">
<h5 class="modal-title text-white" id="lightboxTitle"></h5>
<button class="btn-close btn-close-white"
data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<img id="lightboxImage" src="" class="w-100">
</div>
</div>
</div>
</div>
<!-- Função JavaScript -->
<script>
function openLightbox(imageUrl, title) {
document.getElementById('lightboxImage').src = imageUrl;
document.getElementById('lightboxTitle').textContent = title;
const modal = new bootstrap.Modal(
document.getElementById('lightboxModal')
);
modal.show();
}
</script>
🎨 Efeitos Visuais
Hover effects:
/* Overlay escuro com ícone zoom */
.gallery-overlay {
background: rgba(0, 0, 0, 0.6);
opacity: 0;
transition: opacity 0.3s ease;
}
.gallery-image-wrapper:hover .gallery-overlay {
opacity: 1;
}
.gallery-image-wrapper:hover {
transform: scale(1.02);
}
/* Card hover */
.gallery-card:hover {
transform: translateY(-5px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
/* Animação de filtro */
.gallery-item.hidden {
opacity: 0;
transform: scale(0.9);
position: absolute;
pointer-events: none;
}
📱 Grid Responsivo
| Breakpoint |
Colunas |
Classe Bootstrap |
Altura Imagem |
| ≥992px (Desktop) |
3 colunas |
col-lg-4 |
250px (350px para wide) |
| 576-991px (Tablet) |
2 colunas |
col-sm-6 |
250px |
| <576px (Mobile) |
1 coluna |
(padrão) |
250px |
| Card destaque |
2/3 largura |
col-lg-8 |
350px |
🌐 Fontes das Imagens
Todas as imagens via Unsplash API:
- Thumbs:
?auto=format&fit=crop&w=1200&q=80
- Lightbox:
?auto=format&fit=crop&w=1920&q=90
- Lazy loading: Atributo
loading="lazy" (quando via <img>)
- Background images: Via
style="background-image:url(...)" nos cards
🎯 Categorias e Badges
| Categoria |
Badge |
Quantidade |
| Infraestrutura |
bg-primary |
1 imagem |
| Backend |
bg-success |
2 imagens |
| UI & Dashboard |
bg-warning |
2 imagens |
| Dev Experience |
bg-info |
2 imagens |
| Segurança |
bg-danger |
1 imagem |
🐶 Seed Automático de Imagens
Script dedicado: scripts/seed_gallery_dogs.php
- Usa a API pública Dog CEO para buscar 30 URLs aleatórias de cães
- Baixa cada imagem via cURL e salva em
public/uploads/gallery/
- Insere registros em
gallery_images com títulos e descrições em PT-BR
- Última execução inseriu os IDs
61–90 com sucesso
Execução recomendada:
sudo -u www-data php scripts/seed_gallery_dogs.php
O diretório public/uploads/gallery pertence ao usuário www-data. Para rodar sem sudo -u www-data, ajuste o owner/permissões do diretório ou mantenha a execução como o mesmo usuário do servidor web.
✅ Benefícios da Galeria Interativa:
- Performance: Filtros client-side sem reload, transições CSS puras
- UX aprimorada: Lightbox para visualização ampliada, overlay com ícone intuitivo
- Responsiva: Grid adaptável com Bootstrap, funciona em todos os dispositivos
- Consistência: Mesma estrutura visual do Blog (header, footer, largura)
- Acessibilidade: Badges coloridos, títulos descritivos, modal com close button
- Extensível: Fácil adicionar novas categorias e imagens
Próximas Melhorias Sugeridas:
- Adicionar navegação prev/next dentro do lightbox
- Implementar lazy loading progressivo (IntersectionObserver)
- Upload de imagens via dashboard admin
- Categorias dinâmicas gerenciadas por banco de dados
- Galeria paginada para mais de 20 imagens
- Compartilhamento social de imagens individuais