(85) 99645-7140 nelclassico@gmail.com Praça Coronel Melquiades, 124
Tecnologia

De uniformes de sombreamento a toalhetes Clip-Path: como o GSAP impulsiona meu portfólio | Codrops

Meu nome é Thibault Guignand. Trabalho tanto como freelancer quanto como funcionário em tempo integral (CDI). É uma configuração dupla que me expõe a todo tipo de projeto, desde trabalhos corporativos até projetos totalmente criativos. Meu objetivo de longo prazo é direcionar 100% do meu tempo para o trabalho criativo.

Esse redesenho foi, de certa forma, um laboratório. Uma forma de avaliar minha posição atual no jogo criativo da web e quanto valho nisso. Meu portfólio anterior já trazia a semente dessa direção de design; Eu queria retomar, ir mais longe e alinhá-lo com os anos de experiência acumulada desde então.

Meu consumo diário vem de criativos que acompanho de perto (Aristide Benoist, Cathy Dole, Corentin Bernabou e outros) e de plataformas especializadas como Awwwards. Não olho o trabalho deles para copiar; Eu olho para isso para estabelecer um padrão para mim.

Várias semanas no total. A construção principal foi concluída rapidamente; polir esticou a linha do tempo. Fazer com que todos os formatos (desktop, tablet, celular, movimento reduzido) se comportassem exatamente da maneira que eu queria exigia a maior parte.

Comecei a camada WebGL com Three.js, a escolha óbvia. No meio do caminho, reescrevi tudo em OGL. A compensação valeu a pena: pacote mais leve, API mais enxuta e uma base de código que eu senti que realmente possuía, linha por linha.

Pilha de tecnologia e ferramentas

A pilha é intencionalmente mainstream. Escolher ferramentas amplamente implementadas significa que posso demonstrar domínio dos mesmos blocos de construção que os estúdios já usam.

Vite + React 18 + TypeScript. Resposta padrão para mim agora: loop de desenvolvimento rápido, confiança digitada, surpresa zero para quem lê o código.

GSAP. Fã desde o primeiro dia, e agora que é totalmente gratuito, é óbvio. Amor de coração verde. SplitText , ScrollTrigger e a API da linha do tempo são incomparáveis ​​para o tipo de movimento que desejo.

OGL. Abordado na história de fundo: leveza em relação ao three.js e uma desculpa para se aprofundar no WebGL de baixo nível.

Lenis. A rolagem nativa é muito frágil para animações ScrollTrigger fortemente acopladas. Lenis me oferece uma rolagem suave, além de uma única fonte de verdade que posso sincronizar com o ticker do GSAP.

SCSS + BEM. Hábito e preferência pessoal. Quando estou escrevendo shaders e layouts personalizados, uma convenção de nomenclatura previsível mantém minha cabeça limpa.

i18n (FR/EN). As plataformas de premiação criativa contam com júris internacionais; bilíngue não é opcional se eu quiser que o site seja julgado pelo maior público possível.

Ferramentas de design. Esbocei a grade de layout no Figma para fixar o ritmo e as proporções e depois a enviei como parte do CSS de produção. Pressione Cmd/Ctrl + G em qualquer lugar do site e a grade se sobrepõe no lugar: mesmas calhas, mesmas linhas, mesmas colunas. O conteúdo real fica nessa grade, sem se aproximar dela. Todo o resto (tipografia, movimento, transições) projetei diretamente no código. Menos transferências, ciclo de feedback mais rígido.

Análise de recursos

Transição do carrossel de vídeos

A página inicial fica no topo de um carrossel de vídeos em tela cheia: passe o mouse sobre qualquer retângulo do projeto e o vídeo atual se funde com o próximo através de um padrão de revelação de bloco distorcido por ruído, com uma aberração cromática que atinge o pico no meio da transição.

Como funciona. Um triângulo de tela cheia no espaço NDC executa o fragment shader abaixo. Três coisas acontecem em paralelo:

  1. Máscara de revelação de bloco : UVs são pixelizados e amostrados contra uma textura de ruído estático. Um step() contra o uniforme de progresso transforma isso em uma máscara binária que cresce bloco a bloco. Sem limpeza linear; cada pixel muda abruptamente, mas não simultaneamente.
  2. Deslocamento : uma segunda amostra de ruído, rolando no tempo, distorce os UVs ao longo de uma direção 2D. A intensidade segue parabola(progress, 2.) então a distorção atinge o pico em 50% de progresso e retorna a zero.
  3. Aberração cromática : canais vermelho e azul de ambas as texturas são amostradas com deslocamentos opostos. A mesma flexibilização da parábola.
float dt = parabola(progress, 2.); // Block reveal: static noise pixelated, compared to progress vec2 blockUv = floor(vUv * uNoisePixelSize) / uNoisePixelSize; float noiseVal = texture2D(displacement, blockUv).g; float intpl = step(noiseVal, progress); // Warp vec2 displaceDir = (noise.rg - 0.5) * 2.0; vec2 warpedUv = uv + displaceDir * dt * uDisplaceIntensity; // Chromatic aberration float shift = dt * uRGBShift; t1.r = texture2D(texture1, warpedUv + vec2(shift, 0.0)).r; t1.g = texture2D(texture1, warpedUv).g; t1.b = texture2D(texture1, warpedUv - vec2(shift, 0.0)).b; // same for t2… gl_FragColor = mix(t1, t2, intpl);

GSAP × WebGL: um uniforme como ponte. A coreografia inteira (cada snap de bloco, cada pixel de distorção, cada deslocamento cromático) é conduzida por um único float dt = parabola(progress, 2.); // Block reveal: static noise pixelated, compared to progress vec2 blockUv = floor(vUv * uNoisePixelSize) / uNoisePixelSize; float noiseVal = texture2D(displacement, blockUv).g; float intpl = step(noiseVal, progress); // Warp vec2 displaceDir = (noise.rg - 0.5) * 2.0; vec2 warpedUv = uv + displaceDir * dt * uDisplaceIntensity; // Chromatic aberration float shift = dt * uRGBShift; t1.r = texture2D(texture1, warpedUv + vec2(shift, 0.0)).r; t1.g = texture2D(texture1, warpedUv).g; t1.b = texture2D(texture1, warpedUv - vec2(shift, 0.0)).b; // same for t2… gl_FragColor = mix(t1, t2, intpl); número entre 0 e 1. Esse número reside em JavaScript, interpolado por uma linha do tempo GSAP com uma facilidade personalizada; cada quadro de animação eu copio para o uniforme float dt = parabola(progress, 2.); // Block reveal: static noise pixelated, compared to progress vec2 blockUv = floor(vUv * uNoisePixelSize) / uNoisePixelSize; float noiseVal = texture2D(displacement, blockUv).g; float intpl = step(noiseVal, progress); // Warp vec2 displaceDir = (noise.rg - 0.5) * 2.0; vec2 warpedUv = uv + displaceDir * dt * uDisplaceIntensity; // Chromatic aberration float shift = dt * uRGBShift; t1.r = texture2D(texture1, warpedUv + vec2(shift, 0.0)).r; t1.g = texture2D(texture1, warpedUv).g; t1.b = texture2D(texture1, warpedUv - vec2(shift, 0.0)).b; // same for t2… gl_FragColor = mix(t1, t2, intpl); e o OGL o envia para a GPU. O shader não tem estado, o GSAP possui a curva de movimento e posso trocar atenuações ou encadear linhas de tempo sem tocar em uma linha de GLSL. É o único padrão que reutilizo em todos os efeitos do projeto.

Upload por quadro, o mínimo necessário. As texturas de vídeo devem ser reenviadas a cada quadro, pois o navegador decodificou novos pixels. Sinalizo progress apenas nas duas texturas atualmente envolvidas em uma transição (origem + destino), nunca em todo o pool. Fora de uma transição, o carrossel volta para a reprodução texture.needsUpdate = true nativa com zero uploads de GPU.

Distorção de texto do mapa de fluxo

Em todo o site, títulos de projetos e imagens principais reagem ao cursor com uma distorção fluida e um arco-íris cromático controlado pela velocidade. É o tipo de efeito que morre se você passar o mouse sobre ele, então cada milissegundo conta.

Como funciona. O auxiliar <video> do OGL grava a velocidade do cursor em uma textura RG fora da tela em cada quadro, acumulando uma “pincelada” desbotada de movimento. O shader amostra esse fluxo para distorcer os UVs do texto e, em seguida, faz uma segunda passagem de direcional aberração cromática: em vez de uma mudança RGB simétrica, cada canal é deslocado ao longo do vetor do mouse até o pixel, com diferentes magnitudes por canal (R em 1,5×, G em 0,5×, B em 1,8×).

// Directional chromatic aberration: not centered, guided by cursor vec2 toMouse = vUv - uMouse; float influence = smoothstep(uRadius, 0.0, length(toMouse)) * uVelo; vec2 offset = normalize(toMouse) * influence * uChromaticIntensity; // RGB split sampling float r = texture2D(tWater, baseUV - offset * 1.5).r; float g = texture2D(tWater, baseUV + offset * 0.5).g; float b = texture2D(tWater, baseUV + offset * 1.8).b; // Rainbow kick when velocity is high: sin() with 120° phase offsets if (uVelo > 0.01) { float hueShift = uTime * 0.01 + length(toMouse) * 2.0; r = mix( r, sin(hueShift) * 0.5 + 0.5, uVelo * uColorShift ); g = mix( g, sin(hueShift + 2.094) * 0.5 + 0.5, uVelo * uColorShift ); b = mix( b, sin(hueShift + 4.188) * 0.5 + 0.5, uVelo * uColorShift ); }

O arco-íris usa o truque mais antigo do livro: três chamadas Flowmap separadas por // Directional chromatic aberration: not centered, guided by cursor vec2 toMouse = vUv - uMouse; float influence = smoothstep(uRadius, 0.0, length(toMouse)) * uVelo; vec2 offset = normalize(toMouse) * influence * uChromaticIntensity; // RGB split sampling float r = texture2D(tWater, baseUV - offset * 1.5).r; float g = texture2D(tWater, baseUV + offset * 0.5).g; float b = texture2D(tWater, baseUV + offset * 1.8).b; // Rainbow kick when velocity is high: sin() with 120° phase offsets if (uVelo > 0.01) { float hueShift = uTime * 0.01 + length(toMouse) * 2.0; r = mix( r, sin(hueShift) * 0.5 + 0.5, uVelo * uColorShift ); g = mix( g, sin(hueShift + 2.094) * 0.5 + 0.5, uVelo * uColorShift ); b = mix( b, sin(hueShift + 4.188) * 0.5 + 0.5, uVelo * uColorShift ); } rad, mapeadas para R, G, B. Ele só entra em ação quando o cursor se move rápido o suficiente, o que mantém o efeito silencioso durante o movimento inativo e alto durante movimentos rápidos.

Monte uma vez, troque texturas. Esta é a otimização da qual mais me orgulho. Minha primeira versão montou um novo contexto WebGL para cada título de projeto. Limpo em termos de React, catastrófico na prática. A memória da GPU continuou subindo; o arco-íris vacilou no quarto voo pairado. A reescrita mantém um único sin() montado no nível da página inicial e aceita o destino atual como um 2π/3 prop. O contexto sobrevive, apenas as trocas de textura. Emparelhado com um protetor ocioso que interrompe o loop rAF após 90 quadros sem entrada do cursor (e continua no próximo FlowmapEffect), o efeito custa quase nada quando você não o está usando.

Morph de rolagem do próximo projeto

Na parte inferior de cada página do projeto, a visualização do “Próximo projeto” se expande à medida que você rola: um fundo recortado e ampliado é exibido enquanto um círculo SVG traça um contador de 0→100%. Acerte 100% e você navegará automaticamente para o próximo projeto. Role para cima e tudo inverte e a navegação é cancelada.

Como funciona. Um único imageSrc com mousemove impulsiona a animação. Seu retorno de chamada ScrollTrigger grava quatro valores diretamente no DOM a cada quadro: sem estado React, sem reconciliação.

onUpdate: (self) => { const progress = self.progress; const percent = Math.round(progress * 100); // Counter numberEl.textContent = String(percent >= 99 ? 100 : percent); // Background morph: scale + inset clip-path const bgScale = 1.3 - 0.3 * progress; const insetV = Math.max(0, 20 - 20 * progress); const insetH = Math.max(0, 40 - 40 * progress); bgEl.style.transform = `scale(${bgScale})`; bgEl.style.clipPath = `inset(${insetV}% ${insetH}% ${insetV}% ${insetH}%)`; // SVG progress circle circleEl.style.strokeDashoffset = String(CIRCUMFERENCE - progress * CIRCUMFERENCE); // Auto-navigation check if (percent >= 100 && state === "idle" && hasSeenLowProgress) { // trigger page change } };

Escrever scrub: 1 em vez de chamar onUpdate salva uma árvore de renderização React completa por quadro. Em um laptop de 120 Hz, essa é a diferença entre manteiga e apresentação de slides.

Uma máquina de estado, porque a rolagem é imprevisível. A navegação automática não é tão simples como “alcançar 100% → ir”. As pessoas passam rapidamente pela seção, chegam a um fragmento recarregado em 100% e mudam de ideia no meio do caminho. Acabei com uma máquina de três estados (onUpdate: (self) => { const progress = self.progress; const percent = Math.round(progress * 100); // Counter numberEl.textContent = String(percent >= 99 ? 100 : percent); // Background morph: scale + inset clip-path const bgScale = 1.3 - 0.3 * progress; const insetV = Math.max(0, 20 - 20 * progress); const insetH = Math.max(0, 40 - 40 * progress); bgEl.style.transform = `scale(${bgScale})`; bgEl.style.clipPath = `inset(${insetV}% ${insetH}% ${insetV}% ${insetH}%)`; // SVG progress circle circleEl.style.strokeDashoffset = String(CIRCUMFERENCE - progress * CIRCUMFERENCE); // Auto-navigation check if (percent >= 100 && state === "idle" && hasSeenLowProgress) { // trigger page change } };) mais dois guardas: um sinalizador element.style.* (você só navega automaticamente se realmente rolou de cima para baixo, não se você pousou lá) e um teto de velocidade (se setState() pularmos o gatilho). Role para cima antes do tempo limite de confirmação de 250 ms e idle → triggered → navigating reverte tudo para hasSeenLowProgress. Sem navegações fantasmas.

A mesma animação, dois drivers. A seção também é clicável. Um clique gera uma interpolação GSAP do progresso de rolagem atual para 1 e rola a página para corresponder em paralelo. Mutações DOM idênticas, resultado visual idêntico, apenas uma fonte de tempo diferente. Como o caminho de limpeza já grava tudo por meio de scrub: 1, sequestrá-lo com uma interpolação GSAP levou cerca de dez linhas.

Transições de página (GSAP + Transições de visualização)

Sair da página inicial não é difícil. O plano de fundo do WebGL, a sobreposição de grade, os textos laterais e o cursor personalizado desaparecem juntos; um quarto de segundo depois, a camada de conteúdo segue; em seguida, a API View Transition do navegador assume a transformação final do caminho do clipe. Três tecnologias (GSAP, View Transitions, React) precisam cooperar sem interferir uma na outra.

O pré-carregamento acelera o fadeout. No momento em que um link é clicado, duas coisas começam em paralelo: o fadeout visual e a busca de dados. O onLeaveBack dinâmico do próximo pedaço da rota e o pré-carregamento da imagem principal são acionados antes que o GSAP pinte um único quadro. Quando a linha do tempo termina (~0,6 s), ambos normalmente já pousaram.

// Fire preloads first: they race the GSAP fadeout const chunkReady = chunkPreloaders[routeChunk]().catch(() => {}); const imageReady = project?.heroImage ? preloadImage(project.heroImage) : Promise.resolve(); // Staged fadeout, all parallel with the network const tl = gsap.timeline(); tl.to( [webglBg, gridOverlay, sideTexts, customCursor], { opacity: 0, duration: 0.3, ease: "power2.inOut" }, 0 ).to( contentEl, { opacity: 0, duration: 0.35, ease: "power2.inOut" }, 0.25 ); await tl.then(); await Promise.all([chunkReady, imageReady]); await startPageTransition(() => { flushSync(() => { navigate(path); }); });

idle é o detalhe que faz a transição de visualização funcionar. element.style.* recebe um retorno de chamada, captura o DOM antes de você modificá-lo, executa o retorno de chamada e, em seguida, captura o DOM depois . O import() do React Router é assíncrono, portanto, sem intervenção, o VT captura a página antiga duas vezes e você não obtém animação. Quebrar import() em idle força o React a confirmar a nova rota de forma síncrona dentro do retorno de chamada do VT. Pequeno detalhe, bug irritante se você perder.

Revelação de texto: um padrão usado em todos os lugares

A mesma linguagem de revelação é reproduzida em todo o site. Cada bloco de texto que aparece usa a mesma combinação: um GSAP document.startViewTransition para estrutura de caracteres ou linhas, um efeito de embaralhamento para resolver caracteres e uma limpeza de caminho de clipe em camadas na parte superior. Centralizá-lo em um utilitário significa que um ajuste na curva em um só lugar altera o ritmo de todo o site.

Os dois efeitos são executados juntos, não em sequência. Quando você embaralha uma linha de caracteres aleatórios em direção à string final, a borda esquerda é resolvida primeiro. Colocar em camadas um caminho de clipe que abre da esquerda para a direita na mesma velocidade significa que o usuário só verá a parte do texto que já está legível. Nada se revela como ruído visual; nada revela tudo de uma vez.

gsap.to(lineEl, { duration, ease: "none", scrambleText: { text: lineText, chars: SCRAMBLE_CHARS, revealDelay, speed, }, onStart: () => { // Wipe runs in parallel with the scramble resolve gsap.to(lineEl, { clipPath: "inset(0 0% 0 0)", duration: 0.6, ease: "power2.out", }); }, });

Pré-embaralhar no comprimento certo, travar a altura. Dois detalhes que impedem o layout de respirar durante a animação:

  1. Antes do início da interpolação, cada caractere da string de destino é substituído por um caractere aleatório de navigate(). Os espaços são preservados. A linha ocupa sua largura final antes de ser resolvida, portanto a troca gradual de caracteres não causa refluxo.
  2. A altura pai é bloqueada para sua medida navigate() antes de document.startViewTransition ser executado. SplitText envolve cada linha em seu próprio bloco; sem o bloqueio, o wrapper é brevemente recolhido e o restante da página salta.

O conjunto de caracteres é importante. SplitText. A mistura de letras com pontuação dá à resolução aquela sensação de “decodificação”, em vez de um desbotamento suave entre os alfabetos. O texto parece estar sendo retirado de um buffer.

Design visual e de interação

Um site para pessoas da web. Projetei este portfólio para as pessoas que irão navegar por ele com as ferramentas de desenvolvimento abertas. A linguagem visual deve ser lida como um aperto de mão técnico. Se você sabe o que significa “View Transition API” ou “flowmap”, o site está piscando para você. Esse é o público com quem quero trabalhar.

Efeitos nos quais apoiei em vez de planejar. A aberração cromática não começou como uma decisão estilística. Foi um teste para ver até onde eu poderia levar a amostragem de textura do OGL. Em algum lugar entre a terceira e a quarta iteração, ele parou de parecer uma demonstração de tecnologia e começou a parecer intencional, então eu o mantive e repeti durante a transição do vídeo e ao passar o mouse sobre o fluxo. Tornou-se o fio visual que une o site.

Movimento reduzido, tratado corretamente. As primeiras versões deste site quebraram fortemente em gsap.to(lineEl, { duration, ease: "none", scrambleText: { text: lineText, chars: SCRAMBLE_CHARS, revealDelay, speed, }, onStart: () => { // Wipe runs in parallel with the scramble resolve gsap.to(lineEl, { clipPath: "inset(0 0% 0 0)", duration: 0.6, ease: "power2.out", }); }, });. As palestras de Cassie Evans no GSAP foram o que me fez parar de tratar o movimento reduzido como um sinalizador de incapacidade e começar a tratá-lo como um design paralelo: uma versão real e degradada que ainda transmite a mesma intenção sem o custo vestibular.

Arquitetura e Estrutura

Nada revolucionário aqui. Disciplinado mais que inteligente.

src/ ├── components/ // UI building blocks (custom cursor, minimap, mobile menu, intro, etc.) ├── contexts/ // AppStateContext, WebGLContext ├── data/ // projectsData.ts + image-dimensions.json + lqip-data.json ├── hooks/ // animation & transition logic ├── i18n/ // routes.ts + locales/{fr,en}/*.json ├── pages/ // HomePage, AboutPage, ProjectPage ├── providers/ // LenisProvider ├── services/ // lenisService: singleton synced with GSAP's ticker ├── shaders/ // GLSL, one folder per effect ├── styles/ // SCSS (BEM) ├── utils/ // scrambleText, prefersReducedMotion, imagePreloadCache, viewTransitions └── webgl/ // Sketch.ts, FlowmapEffect.ts: raw OGL

Ganchos vs serviços é a única divisão que importa. Ganchos o próprio ciclo de vida por componente (configuração, limpeza, malabarismo de referência). Os serviços são singletons que sobrevivem a qualquer componente. Lenis tem que sobreviver às mudanças de rota porque a posição de rolagem pertence ao documento, não à página.

Desempenho se resume a quatro hábitos que apliquei em todos os lugares onde algo rodava em cada quadro:

  1. Mutações diretas de DOM (scrub: 1, getBoundingClientRect().height) em vez do estado React. A 120 Hz, uma reconciliação da árvore de renderização é de aproximadamente 8 ms que não tenho.
  2. Objetos vinculados e persistentes : SplitText armazenados em cache uma vez no construtor; o mapa de fluxo montado uma vez na raiz da página; SCRAMBLE_CHARS = 'A!B@C#D$E%F&G*H?J[K]L{M}N=O+P-QRSTUVWXYZ' em vez de prefers-reduced-motion: reduce.
  3. Idle Guards : o loop rAF do mapa de fluxo é suspenso após 90 quadros sem entrada; o carrossel de vídeo volta para as transições externas nativas texture.needsUpdate = true.
  4. Metadados de imagem em tempo de construção : element.style.* (zero CLS) e textContent (desfoques base64 embutidos), além de pré-carregamento de pedaços por rota ao passar o mouse.

Reflexões

Eu coloquei energia nisso. A questão não era apenas enviar um portfólio. Era para fazer a coisa mais acabada que já fiz, forçar cada detalhe (IU, design, acessibilidade, desempenho) até sentir que realmente aprendi algo ao longo do caminho. O objetivo tácito era contribuir, de uma forma minúscula, para elevar o padrão daquilo que as pessoas esperam da web. Não sei se consegui. O que sei é que deixei tudo em campo.

O que funcionou

  • O padrão de mapa de fluxo persistente : montando o componente WebGL uma vez na raiz da página e trocando texturas por meio de um suporte em vez de remontar por projeto. Eliminou completamente uma classe de vazamento de memória.
  • Gerando todos os efeitos WebGL de um único uniforme interpolado GSAP. Depois que o padrão foi clicado, o resto do projeto foi apenas escolher as facilidades.
  • Tratando o movimento reduzido como um design paralelo , não como um substituto. Cassie Evans mudou a forma como penso sobre acessibilidade, e o projeto foi melhor para isso.

O que foi difícil

  • Safari + Ver transições + caminho do clipe. O Safari armazena em cache boundRender = this.render.bind(this) valores nas camadas da GPU, desde que Vec2.set() pseudoelementos estejam ativos. Redefina o valor durante a transição e o Safari o ignorará até você forçar uma repintura. Diagnosticado por acidente, corrigido com um buffer pós-transição de 50 ms e um refluxo forçado (new Vec2()). É o tipo de bug que você não encontra no Stack Overflow porque as palavras-chave certas ainda não existem.
  • O pipeline de pré-carregamento não convergiu na primeira tentativa. Olhando para o log do git, <video> aparece dez vezes seguidas durante algumas semanas. Pré-carregar tudo → muito agressivo, bloqueia a renderização inicial. Pré-carregar nada → pisca. Por fim, cheguei a: pré-carregar as fontes imediatamente, imagem principal ao passar o mouse, primeiro projeto durante a introdução, adiar o resto. Dez se compromete a chegar lá.

O que eu faria de diferente

Comece com OGL, pule o desvio de three.js. Já mencionado na história de fundo, mas em retrospectiva, no dia em que aprendi image-dimensions.json, lqip-data.json, clip-path da fonte do OGL foi o dia em que este projeto realmente começou.

Alcance a sanidade mais cedo. Alguns dos pontos de atrito para os quais criei soluções alternativas (dados do projeto em um arquivo digitado, traduções espalhadas por JSON, recursos de imagem e vídeo manipulados manualmente) são exatamente o que o Sanity foi projetado para resolver. É uma ferramenta genuinamente excepcional e a próxima iteração da camada de conteúdo começará aí.

Deixe um comentário

Seu email não será publicado. Campos obrigatórios marcados com *

Este site utiliza o Akismet para reduzir spam. Saiba como seus dados em comentários são processados.