<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eduardo Villão</title>
    <description>The latest articles on DEV Community by Eduardo Villão (@edu_villao).</description>
    <link>https://dev.to/edu_villao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3900619%2F6107a4b2-168c-4794-a838-97bb3689d596.jpg</url>
      <title>DEV Community: Eduardo Villão</title>
      <link>https://dev.to/edu_villao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/edu_villao"/>
    <language>en</language>
    <item>
      <title>AI doesn't fix direction. It accelerates the one you already have</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:57:20 +0000</pubDate>
      <link>https://dev.to/edu_villao/ai-doesnt-fix-direction-it-accelerates-the-one-you-already-have-4c6n</link>
      <guid>https://dev.to/edu_villao/ai-doesnt-fix-direction-it-accelerates-the-one-you-already-have-4c6n</guid>
      <description>&lt;p&gt;If you're heading in the wrong direction, AI will get you there faster.&lt;/p&gt;

&lt;p&gt;Years of research on how to get the best out of people. Decades of literature on management, leadership, processes, teams, feedback, and organizational culture.&lt;/p&gt;

&lt;p&gt;And now companies are dropping AI into the middle of all that thinking it fixes things.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI doesn't correct your route. It accelerates the one you already have.
&lt;/h2&gt;

&lt;p&gt;If your team delivers well, with clear processes and good communication, AI will amplify that. Results get better, faster, with less friction.&lt;/p&gt;

&lt;p&gt;If your team delivers poorly, with messy processes and weak management, AI will amplify that too. Results get worse, faster, with more invisible friction.&lt;/p&gt;

&lt;p&gt;The tool has no opinion about direction. It just accelerates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What people think AI will fix
&lt;/h2&gt;

&lt;p&gt;Some examples of what happens when you put AI on top of broken structure:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Let's use AI to document our processes."&lt;/strong&gt;&lt;br&gt;
Great. But if the processes don't exist or are a mess, AI will document the mess with polished language and an air of authority. Now the mess has a well-formatted PDF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Let's use AI to give feedback to the team."&lt;/strong&gt;&lt;br&gt;
But if the manager doesn't know what they want from the team, AI will generate vague feedback with the right words. It looks like feedback. It isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Let's use AI to speed up onboarding."&lt;/strong&gt;&lt;br&gt;
But if onboarding has no structure, now you have bad onboarding faster. The new hire gets lost in half the time.&lt;/p&gt;

&lt;p&gt;The pattern is always the same: AI executes well what you ask for. The problem is you don't know exactly what to ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was always the problem is still the problem
&lt;/h2&gt;

&lt;p&gt;AI didn't create new problems in companies. It exposed the ones that were already there.&lt;/p&gt;

&lt;p&gt;Teams that already had standards, clarity, and good management keep delivering well, now with more speed. Teams that didn't keep delivering poorly, now with more speed too, and with the illusion that they're evolving because they're using new tools.&lt;/p&gt;

&lt;p&gt;AI adoption has become a metric for modernity. But modernity isn't installing tools. It's knowing how to use them with intention, inside a structure that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually needs to happen first
&lt;/h2&gt;

&lt;p&gt;Before adding AI to any process, it's worth asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this process work without AI?&lt;/strong&gt; If it doesn't, AI won't fix it. It'll just execute what doesn't work with more efficiency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does whoever is managing this process know what they want as an outcome?&lt;/strong&gt; AI only delivers well when the person asking knows exactly what they're asking for. Vague management leads to vague prompts leads to vague output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the team have clarity on the problem they're solving?&lt;/strong&gt; AI amplifies execution. If execution is heading in the wrong direction, it'll amplify the mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one sentence that sums it all up
&lt;/h2&gt;

&lt;p&gt;If you're heading in the wrong direction, AI will get you there faster.&lt;/p&gt;

&lt;p&gt;AI is an accelerator. Not a course corrector. Not a substitute for management. Not a solution for missing process.&lt;/p&gt;

&lt;p&gt;It's a powerful tool that will get you faster to wherever you were already going.&lt;/p&gt;

&lt;p&gt;Before adding speed, make sure the direction is right.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>career</category>
      <category>programming</category>
    </item>
    <item>
      <title>IA não corrige direção. Ela acelera a que você já tem</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:55:44 +0000</pubDate>
      <link>https://dev.to/edu_villao/ia-nao-corrige-direcao-ela-acelera-a-que-voce-ja-tem-38c</link>
      <guid>https://dev.to/edu_villao/ia-nao-corrige-direcao-ela-acelera-a-que-voce-ja-tem-38c</guid>
      <description>&lt;p&gt;Se você tá indo na direção errada, a IA vai te fazer chegar errado mais rápido.&lt;/p&gt;

&lt;p&gt;Anos de estudo sobre como extrair o melhor das pessoas. Décadas de literatura sobre gestão, liderança, processos, times, feedback, cultura organizacional.&lt;/p&gt;

&lt;p&gt;E agora as empresas estão adicionando IA no meio disso tudo e achando que o problema tá resolvido.&lt;/p&gt;

&lt;p&gt;Não tá.&lt;/p&gt;

&lt;h2&gt;
  
  
  A IA não corrige rota. Ela acelera a rota que você já tem.
&lt;/h2&gt;

&lt;p&gt;Se o seu time entrega bem, com clareza de processo e comunicação boa, a IA vai amplificar isso. O resultado vai ser melhor, mais rápido, com menos atrito.&lt;/p&gt;

&lt;p&gt;Se o seu time entrega mal, com processo confuso e gestão fraca, a IA vai amplificar isso também. O resultado vai ser pior, mais rápido, com mais atrito invisível.&lt;/p&gt;

&lt;p&gt;A ferramenta não tem opinião sobre a direção. Ela só acelera.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que as pessoas acham que a IA vai resolver
&lt;/h2&gt;

&lt;p&gt;Alguns exemplos do que acontece quando alguém coloca IA em cima de estrutura ruim:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Vamos usar IA pra documentar nossos processos."&lt;/strong&gt;&lt;br&gt;
Ótimo. Mas se os processos não existem ou são uma bagunça, a IA vai documentar a bagunça com linguagem bonita e ar de autoridade. Agora a bagunça tem um PDF bem formatado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Vamos usar IA pra dar feedback pra equipe."&lt;/strong&gt;&lt;br&gt;
Mas se o gestor não sabe o que quer do time, a IA vai gerar feedback vago com palavras certas. Parece feedback. Não é.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Vamos usar IA pra acelerar o onboarding."&lt;/strong&gt;&lt;br&gt;
Mas se o onboarding não tem estrutura, agora você tem onboarding ruim mais rápido. O novo funcionário fica perdido em metade do tempo.&lt;/p&gt;

&lt;p&gt;O padrão é sempre o mesmo: a IA executa bem o que você pede. O problema é que você não sabe exatamente o que precisa pedir.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que sempre foi o problema continua sendo o problema
&lt;/h2&gt;

&lt;p&gt;A IA não criou problemas novos nas empresas. Ela escancarou os que já existiam.&lt;/p&gt;

&lt;p&gt;Times que já tinham padrão, clareza e boa gestão continuam entregando bem, agora com mais velocidade. Times que não tinham continuam entregando mal, agora com mais velocidade também, e com a ilusão de que estão evoluindo porque usam ferramentas novas.&lt;/p&gt;

&lt;p&gt;A adoção de IA virou métrica de modernidade. Mas modernidade não é instalar ferramenta. É saber usá-la com intenção, dentro de uma estrutura que funciona.&lt;/p&gt;

&lt;h2&gt;
  
  
  O que realmente precisa acontecer antes
&lt;/h2&gt;

&lt;p&gt;Antes de colocar IA em qualquer processo, vale a pena perguntar:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Esse processo funciona sem IA?&lt;/strong&gt; Se não funciona, a IA não vai consertar. Vai só executar o que não funciona com mais eficiência.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quem está gerindo esse processo sabe o que quer como resultado?&lt;/strong&gt; A IA só entrega bem quando quem pede sabe exatamente o que está pedindo. Gestão vaga gera prompt vago gera output vago.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O time tem clareza sobre o problema que está resolvendo?&lt;/strong&gt; IA amplifica execução. Se a execução está indo na direção errada, ela vai amplificar o erro.&lt;/p&gt;

&lt;h2&gt;
  
  
  A frase que resume tudo
&lt;/h2&gt;

&lt;p&gt;Se você tá indo na direção errada, a IA vai te fazer chegar errado mais rápido.&lt;/p&gt;

&lt;p&gt;A IA é um acelerador. Não é um corretor de rota. Não é um substituto pra gestão. Não é uma solução pra falta de processo.&lt;/p&gt;

&lt;p&gt;É uma ferramenta poderosa que vai fazer você chegar mais rápido aonde você já estava indo.&lt;/p&gt;

&lt;p&gt;Antes de adicionar velocidade, garanta que a direção está certa.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Paying AI to Forget What You Already Know</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Fri, 26 Jun 2026 17:25:59 +0000</pubDate>
      <link>https://dev.to/edu_villao/stop-paying-ai-to-forget-what-you-already-know-3odp</link>
      <guid>https://dev.to/edu_villao/stop-paying-ai-to-forget-what-you-already-know-3odp</guid>
      <description>&lt;p&gt;I'm currently building many apps/things in parallel: a form backend, a WhatsApp review alert tool, my WordPress plugins and a few others. Every single one needs Stripe. Every single one needs transactional emails. Most of them use Cloudflare Workers, D1, and R2.&lt;/p&gt;

&lt;p&gt;For a while, I let AI write those integrations from scratch every time.&lt;/p&gt;

&lt;p&gt;That was expensive. And dumb.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Actually Happening
&lt;/h2&gt;

&lt;p&gt;When you ask Claude Code (or any coding AI) to "integrate Stripe checkout," it doesn't teleport to a finished implementation. It explores. It writes, evaluates, adjusts. It handles errors, then reconsiders the error handling. It figures out where to put the webhook handler, what metadata to attach, how to structure the response.&lt;/p&gt;

&lt;p&gt;All of that costs tokens. And time.&lt;/p&gt;

&lt;p&gt;Now multiply that by every project. Every time I started something new, the AI was rebuilding the same mental model for integrations I had already solved.&lt;/p&gt;

&lt;p&gt;The code usually came out fine. But I was paying in tokens and in review time for decisions that were already made.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: &lt;code&gt;ai-boilerplates&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I created a private repo called &lt;code&gt;ai-boilerplates&lt;/code&gt;. The idea is simple: every integration pattern I've solved and validated goes in there, ready to be used as a reference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-boilerplates/
  stripe/
    checkout.ts
    webhook.ts
    customer.ts
  resend/
    transactional.ts
  cloudflare/
    d1-client.ts
    r2-upload.ts
  auth/
    jwt.ts
    session.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the important part isn't just the code. It's the context block at the top of each file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * BOILERPLATE: Stripe Checkout Session
 *
 * Use: single product or dynamic price_id checkout
 * Don't use: subscriptions (see stripe/subscription.ts)
 *
 * Decisions already made:
 * - success_url always redirects to /dashboard with session_id
 * - metadata always includes userId to reconcile in webhook
 * - errors return 500 with generic message, never expose Stripe details
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That block is the real value. The AI doesn't just copy code. It understands intent, constraints, and prior decisions. It skips the exploration phase entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Into Claude Code
&lt;/h2&gt;

&lt;p&gt;I added a dedicated rule file at &lt;code&gt;~/.claude/rules/ai-boilerplates.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## ai-boilerplates&lt;/span&gt;

Repo at ~/ai-boilerplates/ contains standard boilerplate implementations.

Before implementing any new integration, check if a boilerplate exists.
If it does, use it as the base, don't rewrite from scratch.

When using a boilerplate:
&lt;span class="p"&gt;-&lt;/span&gt; Adapt to the project context, don't copy blindly
&lt;span class="p"&gt;-&lt;/span&gt; Preserve the documented decisions
&lt;span class="p"&gt;-&lt;/span&gt; If you need to deviate from a decision, comment why in the code

When writing code that could become a boilerplate, suggest adding it to the repo.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping it in &lt;code&gt;rules/&lt;/code&gt; rather than &lt;code&gt;CLAUDE.md&lt;/code&gt; is intentional. &lt;code&gt;CLAUDE.md&lt;/code&gt; is for general project context: stack, conventions, who you are. Rules are modular and focused, each with a single responsibility.&lt;/p&gt;

&lt;p&gt;Now every Claude Code session across every project starts with that context loaded. The AI knows to check before it builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;The difference is noticeable in a few ways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fewer tokens on solved problems.&lt;/strong&gt; The AI isn't re-exploring Stripe webhook verification or Resend error handling. It reads the boilerplate, adapts it, moves on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More consistent code across projects.&lt;/strong&gt; The same decisions (metadata structure, error response shape, logging pattern) show up the same way everywhere. That matters when you're context-switching between four codebases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Less review time.&lt;/strong&gt; I'm not second-guessing whether the AI made the right call on something I've already thought through. The call was made once, documented, and that's it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Angle Is Real
&lt;/h2&gt;

&lt;p&gt;AI inference isn't free, and it's only going to get more complex as agents and subagents become the norm. Running parallel agents on a large feature already costs real money. Anything that reduces redundant work compounds across every session, every project, every team member who touches the same stack.&lt;/p&gt;

&lt;p&gt;A boilerplate repo is one of the cheapest optimizations you can make. It costs you an hour to set up and pays back on every single integration after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Goes In a Good Boilerplate
&lt;/h2&gt;

&lt;p&gt;Not every piece of code deserves to be a boilerplate. A good candidate is something you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Integrate in more than one project&lt;/li&gt;
&lt;li&gt;Have already debugged at least once&lt;/li&gt;
&lt;li&gt;Have made explicit architectural decisions about&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all three are true, document it. If you're just writing throwaway code, don't bother.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context Is the Real Asset
&lt;/h2&gt;

&lt;p&gt;Most developers think about AI productivity in terms of prompts: how to ask better questions, how to get better output. That's valid, but it misses the bigger lever.&lt;/p&gt;

&lt;p&gt;The real asset is structured context. Boilerplates are one layer. ADRs are another. Rules that encode how your team works, shared memory of why decisions were made. When that context exists and is loaded into every session, the AI isn't starting from scratch. It's building on top of what you've already figured out.&lt;/p&gt;

&lt;p&gt;That compounds. Every solved problem makes the next one cheaper. Every documented decision saves tokens, review time, and the cognitive overhead of explaining your conventions again.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;ai-boilerplates&lt;/code&gt; repo is a small, concrete place to start. But the habit it builds is what matters.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I migrated a WordPress site to Cloudflare Pages using AI (and what broke)</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Fri, 26 Jun 2026 14:54:40 +0000</pubDate>
      <link>https://dev.to/edu_villao/how-i-migrated-a-wordpress-site-to-cloudflare-pages-using-ai-and-what-broke-eji</link>
      <guid>https://dev.to/edu_villao/how-i-migrated-a-wordpress-site-to-cloudflare-pages-using-ai-and-what-broke-eji</guid>
      <description>&lt;p&gt;WordPress does everything. That's both the problem and the solution.&lt;/p&gt;

&lt;p&gt;I've spent years in the WordPress ecosystem: plugins, themes, WooCommerce, Gutenberg, the whole thing. It's a mature, powerful platform that handles complex cases really well. I'm not here to say it's bad.&lt;/p&gt;

&lt;p&gt;But with AI there's a category of site where it became overkill: landing pages, product sites, company pages, portfolios. Pages that are essentially static content with a contact form. You're running a full PHP stack, managing plugin updates, paying for server hosting, all for a page that could be pure HTML served from the edge.&lt;/p&gt;

&lt;p&gt;For those cases, I've started migrating everything. Destination: Cloudflare Pages. The process: largely AI-assisted.&lt;/p&gt;

&lt;p&gt;Here's how it went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cloudflare Pages
&lt;/h2&gt;

&lt;p&gt;A few reasons it won over Vercel, Netlify, and GitHub Pages for this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global edge network with zero cold starts&lt;/li&gt;
&lt;li&gt;Free tier is genuinely generous — unlimited requests, unlimited bandwidth&lt;/li&gt;
&lt;li&gt;Cloudflare Workers for any dynamic logic that survives the migration&lt;/li&gt;
&lt;li&gt;D1 for lightweight database needs if they come up later&lt;/li&gt;
&lt;li&gt;Everything in one ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're already on Cloudflare for DNS (most people are), the migration path is shorter than it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack after migration
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static site generator:&lt;/strong&gt; Astro — outputs pure HTML, partial hydration for any interactive component, excellent build performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Cloudflare Pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forms:&lt;/strong&gt; FormRoute — more on this later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI used:&lt;/strong&gt; Claude for content transformation, component generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1 — Export and inventory the WordPress content
&lt;/h2&gt;

&lt;p&gt;First step: understand what you actually have.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Export everything from WordPress&lt;/span&gt;
&lt;span class="c"&gt;# Admin → Tools → Export → All content&lt;/span&gt;
&lt;span class="c"&gt;# Downloads an XML file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The XML export gives you posts, pages, categories, tags, authors, and media references. It doesn't give you the actual media files — those come separately.&lt;/p&gt;

&lt;p&gt;I fed the XML export to Claude and asked it to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;List every post and page with title, slug, date, and category&lt;/li&gt;
&lt;li&gt;Identify which content was actively trafficked vs stale&lt;/li&gt;
&lt;li&gt;Flag any posts with embedded shortcodes that would need manual attention&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The output was a clean inventory in about 30 seconds. On a large site this alone saves hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt I used:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;Here's a WordPress XML export. Give me:
1. A table of all posts: title, slug, publish date, category, word count
2. A list of all shortcodes used across the content
3. Any embedded iframes or third-party scripts you can identify
Format as markdown.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2 — Convert content to Markdown
&lt;/h2&gt;

&lt;p&gt;WordPress stores content as HTML with shortcodes mixed in. Astro wants Markdown with frontmatter. Claude handled most of this conversion automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt for content conversion:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;Convert this WordPress post HTML to Markdown with Astro frontmatter.
Preserve all headings, links, images, and formatting.
Output the frontmatter with: title, description, pubDate, slug, tags.
Flag any shortcodes you can't convert with a [MANUAL REVIEW NEEDED] comment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 80% of posts, the output was clean and ready to use. The remaining 20% had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contact form shortcodes (&lt;code&gt;[contact-form-7]&lt;/code&gt;) — needed replacement&lt;/li&gt;
&lt;li&gt;Gallery shortcodes (&lt;code&gt;[gallery ids="..."]&lt;/code&gt;) — needed manual rebuild&lt;/li&gt;
&lt;li&gt;Plugin-specific shortcodes — varied by plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shortcode problem is where most WordPress migrations get stuck. More on the form replacement below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Set up Astro on Cloudflare Pages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create astro@latest my-site
&lt;span class="nb"&gt;cd &lt;/span&gt;my-site
npx astro add cloudflare
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Cloudflare adapter handles the build output format for Pages. After that, connect your GitHub repo to Cloudflare Pages and it deploys on every push.&lt;/p&gt;

&lt;p&gt;Basic &lt;code&gt;astro.config.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cloudflare&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/cloudflare&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;static&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cloudflare&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a pure static site, &lt;code&gt;output: 'static'&lt;/code&gt; is what you want. No server-side rendering, no cold starts, pure HTML at the edge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Migrate content and assets
&lt;/h2&gt;

&lt;p&gt;I asked Claude to generate a migration script that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the WordPress XML export&lt;/li&gt;
&lt;li&gt;Created individual &lt;code&gt;.md&lt;/code&gt; files for each post with proper frontmatter&lt;/li&gt;
&lt;li&gt;Renamed media files to a clean slug-based convention&lt;/li&gt;
&lt;li&gt;Updated internal image references in the content
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Claude-generated migration script (simplified)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parseStringPromise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xml2js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slugify&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;export.xml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parseStringPromise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content:encoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pubDate&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wp:post_name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`---
title: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"
pubDate: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;
slug: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"
---\n\n`&lt;/span&gt;

  &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`src/content/blog/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.md`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;frontmatter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;convertToMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For media, I kept the files in the Cloudflare Pages public folder and updated the references in the Markdown files. For larger sites with heavy media, R2 is worth looking at, but for most company pages and landing pages, serving assets directly from Pages is enough. Claude generated the find-and-replace script for URL rewriting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — The forms problem
&lt;/h2&gt;

&lt;p&gt;This is where every WordPress-to-static migration hits the same wall.&lt;/p&gt;

&lt;p&gt;Contact Form 7, Gravity Forms, WPForms — they all depend on PHP running on the server. Move to static and they stop working entirely. There's no server to process the submission.&lt;/p&gt;

&lt;p&gt;The options I evaluated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write a Cloudflare Worker&lt;/strong&gt; — possible, but you're now maintaining a backend for what is essentially a contact form. Wiring up email delivery, validation, spam protection. More moving parts than I wanted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a form backend service&lt;/strong&gt; — drop one endpoint into the form, the service handles everything. No server, no maintenance.&lt;/p&gt;

&lt;p&gt;I went with &lt;a href="https://formroute.dev" rel="noopener noreferrer"&gt;FormRoute&lt;/a&gt;. The migration from Contact Form 7 was straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (Contact Form 7 shortcode):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[contact-form-7 id="123" title="Contact form"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (Astro component):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/components/ContactForm.astro
---

&amp;lt;form action="https://api.formroute.dev/f/YOUR_KEY" method="POST"&amp;gt;
  &amp;lt;input type="text" name="name" placeholder="Your name" required /&amp;gt;
  &amp;lt;input type="email" name="email" placeholder="Your email" required /&amp;gt;
  &amp;lt;textarea name="message" placeholder="Your message" required&amp;gt;&amp;lt;/textarea&amp;gt;

  &amp;lt;div class="cf-turnstile" data-sitekey="YOUR_TURNSTILE_KEY"&amp;gt;&amp;lt;/div&amp;gt;

  &amp;lt;input type="text" name="_honeypot" style="display:none" tabindex="-1" /&amp;gt;

  &amp;lt;button type="submit"&amp;gt;Send&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spam protection runs via Cloudflare Turnstile, invisible to real users. Submissions go to the FormRoute dashboard and trigger an email notification. Free tier covers 1,000 submissions a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Redirects
&lt;/h2&gt;

&lt;p&gt;WordPress URLs don't always match what you want for a static site. You need to redirect old URLs to new ones so you don't lose SEO or break existing links.&lt;/p&gt;

&lt;p&gt;Cloudflare Pages handles redirects via a &lt;code&gt;_redirects&lt;/code&gt; file in the public folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;/old-post-url/&lt;/span&gt; &lt;span class="n"&gt;/new-post-url/&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;
&lt;span class="n"&gt;/category/news/&lt;/span&gt; &lt;span class="n"&gt;/blog/&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;
&lt;span class="n"&gt;/?p=123&lt;/span&gt; &lt;span class="n"&gt;/post-slug/&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I fed Claude the old URL list and the new slug list and asked it to generate the redirects file. It handled the mapping in one pass.&lt;/p&gt;

&lt;p&gt;For WordPress sites with &lt;code&gt;?p=123&lt;/code&gt; style URLs, the pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;/?p=:id&lt;/span&gt; &lt;span class="n"&gt;/blog/:slug&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need to map each ID to its slug manually or via a script — Claude can generate that script from the XML export.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke (honest list)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Comments&lt;/strong&gt; — WordPress comments don't have a static equivalent. I replaced them with nothing. If comments matter for your site, look at Giscus (GitHub Discussions-based) or just remove them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt; — WordPress search is server-side. Static sites need client-side search. Pagefind works well with Astro and takes 10 minutes to set up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WooCommerce&lt;/strong&gt; — if your WordPress site has ecommerce, this migration path doesn't apply. WooCommerce requires a server. Static + Shopify Buy Button or a headless approach is the alternative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some SEO plugins&lt;/strong&gt; — Yoast SEO data lives in WordPress meta. The XML export includes it but you need to map it manually to your Astro frontmatter. Claude can generate the mapping script but it takes some cleanup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin-specific shortcodes&lt;/strong&gt; — anything from a plugin that isn't content (countdown timers, booking widgets, interactive maps) needs a replacement. There's no universal answer here — depends on what the plugin did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build time:&lt;/strong&gt; under 10 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to first byte:&lt;/strong&gt; under 50ms globally (Cloudflare edge)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse score:&lt;/strong&gt; 98–100 across the board&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting cost:&lt;/strong&gt; $0 (Cloudflare Pages free tier)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance:&lt;/strong&gt; zero PHP, zero plugin updates, zero server patches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI-assisted migration took about a day of actual work for a mid-size site (80 posts, 20 pages). Without AI the same work would have taken a week — content conversion and script generation were the biggest time savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary — what AI helped with
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Time without AI&lt;/th&gt;
&lt;th&gt;Time with AI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Content inventory&lt;/td&gt;
&lt;td&gt;2–3 hours&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML to Markdown conversion&lt;/td&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration scripts&lt;/td&gt;
&lt;td&gt;4–6 hours&lt;/td&gt;
&lt;td&gt;10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirects file&lt;/td&gt;
&lt;td&gt;2 hours&lt;/td&gt;
&lt;td&gt;5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component generation&lt;/td&gt;
&lt;td&gt;3 hours&lt;/td&gt;
&lt;td&gt;20 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The parts AI couldn't help with: judgment calls on what to keep, what to cut, and how to handle plugin-specific functionality. That's still on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.astro.build" rel="noopener noreferrer"&gt;Astro docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/pages" rel="noopener noreferrer"&gt;Cloudflare Pages docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://formroute.dev" rel="noopener noreferrer"&gt;FormRoute&lt;/a&gt; — form backend for static sites&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>wordpress</category>
      <category>ai</category>
      <category>webdev</category>
      <category>astro</category>
    </item>
    <item>
      <title>How to add a contact form to a Next.js app without a backend</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Fri, 26 Jun 2026 13:34:47 +0000</pubDate>
      <link>https://dev.to/edu_villao/how-to-add-a-contact-form-to-a-nextjs-app-without-a-backend-317g</link>
      <guid>https://dev.to/edu_villao/how-to-add-a-contact-form-to-a-nextjs-app-without-a-backend-317g</guid>
      <description>&lt;p&gt;If you've ever built a Next.js app and needed a contact form, you've probably hit the same wall: you don't want to set up a backend just to receive an email.&lt;/p&gt;

&lt;p&gt;You have a few options. You could write an API route, wire up an email service like Resend or SendGrid, handle validation, add spam protection, deploy, and maintain it forever. Or you could use a form backend service and skip all of that.&lt;/p&gt;

&lt;p&gt;This tutorial shows the second path. We'll build a working contact form in Next.js — App Router — that validates fields, blocks spam, and delivers submissions to your inbox. No backend, no server to maintain.&lt;/p&gt;

&lt;p&gt;We'll use FormRoute as the form backend. Free tier covers 1,000 submissions a month with a dashboard included.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A contact form with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name, email, and message fields&lt;/li&gt;
&lt;li&gt;Client-side and server-side validation&lt;/li&gt;
&lt;li&gt;Spam protection with Cloudflare Turnstile&lt;/li&gt;
&lt;li&gt;Success and error states&lt;/li&gt;
&lt;li&gt;Zero backend code&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Create your FormRoute endpoint
&lt;/h2&gt;

&lt;p&gt;Go to &lt;a href="https://formroute.dev" rel="noopener noreferrer"&gt;formroute.dev&lt;/a&gt; and create a free account. After signup, create a new form and copy your endpoint URL. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://api.formroute.dev/f/YOUR_KEY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the only setup you need on the FormRoute side.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Add Turnstile to your page
&lt;/h2&gt;

&lt;p&gt;FormRoute uses Cloudflare Turnstile for spam protection. It's invisible to real users — no "click the traffic lights" — and it's handled automatically when you add two lines to your page.&lt;/p&gt;

&lt;p&gt;Add the Turnstile script to your &lt;code&gt;layout.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Script&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/script&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Script&lt;/span&gt;
          &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"https://challenges.cloudflare.com/turnstile/v0/api.js"&lt;/span&gt;
          &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"lazyOnload"&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;html&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Build the form component
&lt;/h2&gt;

&lt;p&gt;Create a new component &lt;code&gt;app/components/ContactForm.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ContactForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setErrors&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLFormElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLFormElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cf-turnstile-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cf-turnstile-response&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Basic client-side validation&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newErrors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;newErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;newErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Valid email is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;newErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Message must be at least 10 characters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newErrors&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newErrors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setErrors&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.formroute.dev/f/YOUR_KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submission failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;formRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Message sent.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;We'll get back to you as soon as possible.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;formRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;noValidate&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Name&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Message&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
          &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Turnstile widget — invisible to most users */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"cf-turnstile"&lt;/span&gt;
        &lt;span class="na"&gt;data-sitekey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_FORMROUTE_TURNSTILE_KEY"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Honeypot — do not remove */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"_honeypot"&lt;/span&gt;
        &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;autoComplete&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Send message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Something went wrong. Please try again.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Use the component
&lt;/h2&gt;

&lt;p&gt;Add the form to any page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/contact/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ContactForm&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/ContactForm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ContactPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Contact&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContactForm&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. How server-side validation works
&lt;/h2&gt;

&lt;p&gt;Even though we validate on the client, FormRoute also validates every submission on the server — on the edge, before anything is stored or forwarded.&lt;/p&gt;

&lt;p&gt;You configure validation rules once in your FormRoute dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required|email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required|min:2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required|min:10"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means even if someone bypasses your frontend and POSTs directly to your endpoint, garbage doesn't reach your inbox. Both layers run independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. What happens after submission
&lt;/h2&gt;

&lt;p&gt;Every valid submission:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Passes Turnstile + honeypot check&lt;/li&gt;
&lt;li&gt;Passes server-side validation rules&lt;/li&gt;
&lt;li&gt;Gets stored in your FormRoute dashboard (up to 30 days, configurable)&lt;/li&gt;
&lt;li&gt;Triggers an email notification to your inbox&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can review, search, and export submissions from the dashboard at any time. If an email notification falls into spam, your submissions are still there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you skipped
&lt;/h2&gt;

&lt;p&gt;By using FormRoute instead of a custom API route, you skipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up an email service (Resend, SendGrid, SES)&lt;/li&gt;
&lt;li&gt;Writing validation logic server-side&lt;/li&gt;
&lt;li&gt;Handling spam protection infrastructure&lt;/li&gt;
&lt;li&gt;Deploying and maintaining an API endpoint&lt;/li&gt;
&lt;li&gt;Debugging email deliverability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The form works. You ship the feature and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plain HTML version
&lt;/h2&gt;

&lt;p&gt;If you're not using React, the same result in plain HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://api.formroute.dev/f/YOUR_KEY"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cf-turnstile"&lt;/span&gt; &lt;span class="na"&gt;data-sitekey=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_TURNSTILE_KEY"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"_honeypot"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt; &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"-1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://challenges.cloudflare.com/turnstile/v0/api.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;Contact forms are one of those features that look simple but hide complexity. Validation, spam, deliverability, storage — each one is a small problem that adds up.&lt;/p&gt;

&lt;p&gt;A form backend handles all of it so you don't have to. The tradeoff is a dependency on a third-party service — but for most projects, that's a good tradeoff.&lt;/p&gt;

&lt;p&gt;FormRoute free tier covers 1,000 submissions a month, includes a dashboard, and has no time limit. &lt;a href="https://formroute.dev" rel="noopener noreferrer"&gt;formroute.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>AI Without Standards Is Just Faster Chaos</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:46:28 +0000</pubDate>
      <link>https://dev.to/edu_villao/ai-without-standards-is-just-faster-chaos-2nie</link>
      <guid>https://dev.to/edu_villao/ai-without-standards-is-just-faster-chaos-2nie</guid>
      <description>&lt;p&gt;Every engineering team I talk to is using AI. Nobody's debating that anymore.&lt;/p&gt;

&lt;p&gt;What nobody's talking about is that every developer on the same team is using it differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Admits
&lt;/h2&gt;

&lt;p&gt;Ten developers. Ten different setups. Ten different rules files. Ten different prompting habits. Same codebase, same tickets, same sprint - ten different approaches to how AI touches the code.&lt;/p&gt;

&lt;p&gt;One dev has a meticulous CLAUDE.md with architecture decisions, testing conventions, and code style rules. The dev next to them has nothing - just vibes and a blank context window. Both ship code. Both pass review. The inconsistency is invisible until it isn't.&lt;/p&gt;

&lt;p&gt;AI didn't create this problem. It amplified what was already broken: the lack of shared engineering standards that actually get enforced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Things That Break
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No visibility.&lt;/strong&gt; You don't know how your team uses AI. Who follows best practices. Who doesn't. What gets reviewed. What ships blind. Engineering managers have dashboards for deployment frequency, test coverage, PR cycle time - but zero visibility into how AI is shaping the code their team writes every day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No consistency.&lt;/strong&gt; Same ticket, ten different approaches. One dev asks the AI to write tests first. Another skips tests entirely and asks for the implementation. A third gives the AI the full architectural context. A fourth gives it nothing. The output varies wildly - not because the AI is inconsistent, but because the humans are.&lt;/p&gt;

&lt;p&gt;And this isn't just about different code styles. It's about where in the codebase the change happens, what the scope should be, why this approach and not another. There are a thousand ways to solve the same problem. The AI will confidently execute any of them. Without a shared standard for how your team makes these decisions, every AI-generated PR becomes a review burden. Someone has to undo, redo, or reshape work that was technically correct but architecturally wrong. That's rework at scale, and it compounds fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowledge walks out.&lt;/strong&gt; When a developer leaves, their context, patterns, and shortcuts leave with them. The rules they built, the prompts they refined, the architectural decisions they encoded in their local setup - gone. The next hire starts from zero. This was already a problem before AI. Now it's worse, because the amount of implicit knowledge in a developer's AI setup is massive and completely undocumented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Harder Than It Looks
&lt;/h2&gt;

&lt;p&gt;The obvious reaction is "just create a shared rules file." That's where most teams start. It's also where most teams stop.&lt;/p&gt;

&lt;p&gt;A shared rules file in a repo solves the format problem but not the enforcement problem. Nobody checks if developers actually use it. Nobody knows if it's up to date. Nobody knows if the dev who joined last month even knows it exists.&lt;/p&gt;

&lt;p&gt;And rules are just the beginning. What about which models are approved? What about which phases of work should use AI and which shouldn't? What about cost? What about the architectural decisions that should constrain how AI generates code in your specific codebase?&lt;/p&gt;

&lt;p&gt;Standardizing AI in an engineering team isn't a file. It's a system. And most teams don't have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Needs to Happen
&lt;/h2&gt;

&lt;p&gt;I think about this in three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Shared context.&lt;/strong&gt; Every developer working on the same codebase should start with the same foundational context. Architecture decisions, code conventions, testing strategy, dependency rules - this isn't optional, and it shouldn't depend on individual setup. If your ADRs exist but only two people know where they are, they don't exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Guardrails.&lt;/strong&gt; Not everything should be delegated to AI. Some decisions require human judgment. Some code paths are too critical for unsupervised generation. The team needs to define where AI adds value and where it adds risk - and enforce that distinction, not just document it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Visibility.&lt;/strong&gt; You need to know what's happening. Not surveillance - signal. Which parts of the codebase are AI-touched. What patterns are emerging. Where the inconsistencies are. Without this, you're managing a process you can't see.&lt;/p&gt;

&lt;p&gt;Most teams have none of these layers. Some have a partial Layer 1. Almost nobody has Layer 2 or 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Competitive Advantage
&lt;/h2&gt;

&lt;p&gt;Here's what I think most people miss: the competitive advantage of AI in engineering isn't speed. Speed is table stakes. Everyone gets faster.&lt;/p&gt;

&lt;p&gt;The advantage is in how consistently and reliably you use it across the entire team. The team that ships fast with shared standards will outperform the team that ships fast with ten different approaches - because the second team is accumulating invisible inconsistency that compounds over time.&lt;/p&gt;

&lt;p&gt;Faster chaos is still chaos. It just takes longer to notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  This Isn't About Tools
&lt;/h2&gt;

&lt;p&gt;I'm not pitching a solution here. I'm describing a problem I see everywhere and that I think is underexplored.&lt;/p&gt;

&lt;p&gt;The industry spent the last two years talking about adopting AI. The next conversation needs to be about governing it. Not in a bureaucratic, compliance-heavy way - in a practical, engineering-first way. Shared context, clear guardrails, basic visibility.&lt;/p&gt;

&lt;p&gt;Every team that adopted AI without standardizing it is running an experiment with no controls. Some of those experiments will work out fine. Some won't. The ones that don't will be very expensive to fix - because the technical debt from inconsistent AI usage is invisible until it's systemic.&lt;/p&gt;

&lt;p&gt;The question isn't whether your team should use AI. It's whether they should all use it the same way.&lt;/p&gt;

&lt;p&gt;I think the answer is obvious.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Self-Host WordPress Plugins on GitHub and Deliver Updates</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:21:37 +0000</pubDate>
      <link>https://dev.to/edu_villao/how-to-self-host-wordpress-plugins-on-github-and-deliver-updates-4745</link>
      <guid>https://dev.to/edu_villao/how-to-self-host-wordpress-plugins-on-github-and-deliver-updates-4745</guid>
      <description>&lt;p&gt;Managing plugin updates can be a challenge, especially if you're not relying on the WordPress Plugin Repository. But what if you could self-host your plugins on GitHub and deliver updates seamlessly, without the need for complex libraries or third-party services?&lt;/p&gt;

&lt;p&gt;In this guide, I'll walk you through a simple yet powerful solution that adheres to WordPress's standard for updates, making the process completely transparent for your users. To make this possible, I've developed a custom GitHub Action and a PHP script that work together to handle updates effortlessly. This is the same solution I use for some of my own plugins, and now I'm sharing it with the community so you can benefit from it too.&lt;/p&gt;

&lt;p&gt;No extra dependencies, no fuss — just GitHub and a bit of PHP magic. Let's get started! 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Self-Hosting Plugins?
&lt;/h2&gt;

&lt;p&gt;Before diving in, let's take a step back: self-hosting plugins means managing your plugin's storage and updates independently, outside the WordPress.org repository. You take control of where your WordPress plugins are stored and how updates are delivered, without relying on the official WordPress Plugin Repository. Instead of hosting your plugin on WordPress.org, you use your own infrastructure — such as GitHub, a private server, or any other file host — to manage your plugin's lifecycle.&lt;/p&gt;

&lt;p&gt;In essence, self-hosting empowers developers to build, distribute, and update plugins on their own terms, while still providing users with a seamless and familiar update experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Required?
&lt;/h2&gt;

&lt;p&gt;At a high level, you'll need two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A server (in our case GitHub):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host metadata about your plugin, including the latest version, download URL, and other required information.&lt;/li&gt;
&lt;li&gt;Store the &lt;code&gt;.zip&lt;/code&gt; distribution file, which will be downloaded during the update process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. An Update Checker Script in Your Plugin:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fetch data from the server (GitHub) to retrieve the latest plugin details.&lt;/li&gt;
&lt;li&gt;Compare versions to check whether the installed version is outdated.&lt;/li&gt;
&lt;li&gt;Forward updates seamlessly into WordPress's default update system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these two components working together, you can deliver a smooth and automated update experience for your plugins, all while retaining full control over the distribution process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Server Side on GitHub
&lt;/h2&gt;

&lt;p&gt;The GitHub Action automates the creation of everything needed for the plugin update process. Here's the high-level flow:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Generate JSON Metadata
&lt;/h3&gt;

&lt;p&gt;The action parses your &lt;code&gt;readme.txt&lt;/code&gt; and other relevant files to create a JSON file containing metadata about your plugin — version, download URL, description, and more. This metadata is essential for the PHP script on the plugin side to query the server and verify if the plugin is up to date.&lt;/p&gt;

&lt;p&gt;The download URL is automatically generated to point directly to the &lt;code&gt;.zip&lt;/code&gt; file in the release created by the action, ensuring WordPress fetches updates directly from GitHub.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Prepare the Distribution Package
&lt;/h3&gt;

&lt;p&gt;The action compiles your plugin files into a &lt;code&gt;.zip&lt;/code&gt; package, ready for distribution. During this process, specific rules can be defined to exclude unnecessary folders or files — such as development directories, test cases, or build artifacts. This ensures a clean and optimized distribution file.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Release Creation
&lt;/h3&gt;

&lt;p&gt;Once the metadata and distribution package are prepared, the action automates the creation of a new GitHub Release. The release is tagged with the corresponding plugin version, ensuring WordPress and the PHP script can correctly fetch the latest version.&lt;/p&gt;

&lt;p&gt;This streamlined process ensures that every time you create a new tag/version of your plugin, all required files and metadata are automatically prepared and hosted, ready to deliver updates to your users.&lt;/p&gt;

&lt;p&gt;👉 Check the full implementation: &lt;a href="https://github.com/eduardovillao/wp-self-host-updater-generator" rel="noopener noreferrer"&gt;wp-self-host-updater-generator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plugin Side — Update Checker
&lt;/h2&gt;

&lt;p&gt;The PHP script serves as the "bridge" between the plugin installed on the WordPress site and the server-side metadata hosted on GitHub. Here's how it operates:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add a Simple PHP File to Manage Updates
&lt;/h3&gt;

&lt;p&gt;The update checker script is integrated directly into your plugin. It hooks into WordPress's native update events via filters like &lt;code&gt;plugins_api&lt;/code&gt; and &lt;code&gt;site_transient_update_plugins&lt;/code&gt; to manage and provide update information dynamically.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Request JSON Data from GitHub
&lt;/h3&gt;

&lt;p&gt;The script queries the GitHub-hosted JSON metadata file for the latest information about your plugin — current version, description, download URL, and other details. This ensures the site always has access to accurate, up-to-date plugin information.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Compare Current Version vs. Latest Version
&lt;/h3&gt;

&lt;p&gt;Once the JSON data is retrieved, the script compares the currently installed version with the version available in the metadata. If the versions match, no action is taken. If a newer version exists, the script forwards the update information to WordPress's built-in update system.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Follow the Default WordPress Flow to Upgrade
&lt;/h3&gt;

&lt;p&gt;If a new version is available, WordPress takes over using its default upgrade mechanism. This ensures a seamless and familiar experience for end users, who can update the plugin just like they would with any other WordPress plugin.&lt;/p&gt;

&lt;p&gt;👉 Check the full implementation: &lt;a href="https://github.com/eduardovillao/wp-self-host-updater-checker" rel="noopener noreferrer"&gt;wp-self-host-updater-checker&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;And that's it! 🎉&lt;/p&gt;

&lt;p&gt;If you have any questions, need help with implementation, or have tested this solution and want to share feedback, feel free to drop a comment below. I'd love to hear from you!&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Will this work with mu-plugins?&lt;/strong&gt;&lt;br&gt;
Partially! The server side on GitHub is fully capable of managing MU-Plugins. However, since MU-Plugins don't follow the same update flow as regular WordPress plugins, the PHP script will require some adjustments. Coming soon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Will this work with themes?&lt;/strong&gt;&lt;br&gt;
Not yet. Some modifications are needed to support themes. For now, this solution works exclusively with plugins — theme support is in progress.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Can I validate the user license before delivering updates?&lt;/strong&gt;&lt;br&gt;
This flow is designed for "free" plugins, so license validation isn't currently supported. I'm exploring ways to incorporate this feature and will share updates soon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Will this work with private repositories?&lt;/strong&gt;&lt;br&gt;
Almost! The action works as-is, but the PHP script needs changes because a token is required to authenticate requests for the JSON data from private repos. Details coming soon.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>github</category>
      <category>php</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Handle Elementor Popup Events Without jQuery</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:20:15 +0000</pubDate>
      <link>https://dev.to/edu_villao/handle-elementor-popup-events-without-jquery-5736</link>
      <guid>https://dev.to/edu_villao/handle-elementor-popup-events-without-jquery-5736</guid>
      <description>&lt;p&gt;If you've ever worked with Elementor and tried to manipulate its popups programmatically, you've probably noticed that the official documentation provides event handling examples only with jQuery. However, if you prefer a modern and lightweight solution using Vanilla JavaScript, this guide is for you.&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;MutationObserver&lt;/code&gt; API, you can monitor changes to the DOM and detect when an Elementor popup modal is injected into the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. This allows you to execute custom actions without relying on additional libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use MutationObserver?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;MutationObserver&lt;/code&gt; API is a native JavaScript feature that lets you observe DOM changes, such as adding or removing elements, and modifications to attributes of existing elements.&lt;/p&gt;

&lt;p&gt;In the case of Elementor popups, it's perfect for detecting when the popup modal is dynamically added to the DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code to Detect Elementor Popups
&lt;/h2&gt;

&lt;p&gt;Here's a complete code snippet that you can use in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Select the &amp;lt;body&amp;gt; element&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create a MutationObserver&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;mutations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Check if new nodes were added&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;childList&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addedNodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Check if the added node is an Elementor popup modal&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elementor-popup-modal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Elementor popup detected:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="c1"&gt;// Add your custom logic here&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Configure the observer to monitor the &amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;childList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Stop observing when no longer needed (optional)&lt;/span&gt;
&lt;span class="c1"&gt;// observer.disconnect();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Monitoring &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;:&lt;/strong&gt; The &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; element is observed because Elementor injects its popups as child nodes of &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MutationObserver:&lt;/strong&gt; We use the &lt;code&gt;MutationObserver&lt;/code&gt; to watch for changes in the child nodes of &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; by setting &lt;code&gt;{ childList: true }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filtering Added Nodes:&lt;/strong&gt; For each added node, we check if it has the class &lt;code&gt;elementor-popup-modal&lt;/code&gt;, which identifies Elementor's popups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom Actions:&lt;/strong&gt; When the modal is detected, you can execute any logic in the block where the &lt;code&gt;console.log&lt;/code&gt; statement is located.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Use Case
&lt;/h2&gt;

&lt;p&gt;Suppose you want to apply a custom animation when an Elementor popup appears. You can modify the code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elementor-popup-modal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Elementor popup detected:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Start invisible&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opacity 0.5s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Fade-in effect&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Choose Vanilla JS?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;No Dependencies:&lt;/strong&gt; Reduces the overall project size by eliminating libraries like jQuery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improved Performance:&lt;/strong&gt; Vanilla JavaScript is generally faster since it doesn't have the overhead of handling selectors and events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modern Compatibility:&lt;/strong&gt; &lt;code&gt;MutationObserver&lt;/code&gt; is supported in all modern browsers, including Edge and Safari.&lt;/p&gt;




&lt;p&gt;Using &lt;code&gt;MutationObserver&lt;/code&gt; to detect Elementor popups is an elegant and efficient solution, especially for developers who prefer not to rely on jQuery. With this code, you can fully customize how you interact with Elementor popups, whether it's adding animations, tracking events, or implementing other custom logic.&lt;/p&gt;

&lt;p&gt;If you found this tip helpful, share it with other developers and leave your ideas for popup customization in the comments!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>wordpress</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>In the AI Era, Soft Skills Are the Hard Skills</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:15:00 +0000</pubDate>
      <link>https://dev.to/edu_villao/in-the-ai-era-soft-skills-are-the-hard-skills-3ilh</link>
      <guid>https://dev.to/edu_villao/in-the-ai-era-soft-skills-are-the-hard-skills-3ilh</guid>
      <description>&lt;p&gt;The more powerful AI gets, the more human the bottleneck becomes. You open a new chat, type a vague prompt, get a mediocre response, and blame the model. The model isn't the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed
&lt;/h2&gt;

&lt;p&gt;For the past decade, the most valuable thing a developer could do was execute. Write the code, ship the feature, fix the bug. Speed and technical precision were the differentiators.&lt;/p&gt;

&lt;p&gt;AI didn't just speed that up. It changed who does it.&lt;/p&gt;

&lt;p&gt;A well-prompted AI agent can write a working feature in minutes. It can refactor a module, generate tests, handle edge cases, and document the result. All before you've finished your second coffee. The execution layer is no longer the bottleneck.&lt;/p&gt;

&lt;p&gt;What's left? The part AI can't do on its own: figuring out what to build, why it matters, and how to frame it clearly enough that something else can execute it well.&lt;/p&gt;

&lt;p&gt;The bottleneck shifted from execution to clarity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Clarity Actually Means
&lt;/h2&gt;

&lt;p&gt;Clarity isn't just "communicate better." That's too vague to be useful.&lt;/p&gt;

&lt;p&gt;In practice, clarity means:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowing what to build.&lt;/strong&gt; Not every feature request deserves to be built. Not every bug deserves to be fixed right now. The ability to evaluate, prioritize, and say no is a skill. It becomes more valuable when execution is cheap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowing when to build it.&lt;/strong&gt; Sequencing matters. Building the right thing in the wrong order creates rework. AI amplifies this problem: it can generate a lot of code very fast, and if the direction is wrong, you have a lot of wrong code very fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowing what to delegate.&lt;/strong&gt; Not everything should go to AI. Some decisions require human judgment, context, or accountability. The developer who throws everything at the model and hopes for the best will produce unstable, expensive, hard-to-maintain systems. Knowing the limits of delegation is a skill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Knowing how to frame it.&lt;/strong&gt; This is where most people underestimate the work. A vague brief produces a vague result. The better you can define the problem — constraints, expected output, edge cases, context — the better the output. That's not prompting as a trick. That's communication as a discipline.&lt;/p&gt;

&lt;p&gt;None of this is technical in the traditional sense. All of it determines whether the technical output is any good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing AI Is Managing People
&lt;/h2&gt;

&lt;p&gt;Here's the thing: none of this is new.&lt;/p&gt;

&lt;p&gt;The best practices for working with AI are basically textbook management principles. Break work into smaller tasks. Give clear scope before delegating. Review output before shipping. Don't overload with context. One thing at a time.&lt;/p&gt;

&lt;p&gt;Experienced managers figured this out with humans. The difference is that most developers never had to manage anyone. They just had to code. Now they're managing an AI that can out-execute them technically, and they're discovering management the hard way: by getting bad results and wondering why.&lt;/p&gt;

&lt;p&gt;The developer who spent years ignoring team dynamics, documentation, and clear communication now has a gap. The developer who built those habits, even informally, has an advantage they didn't expect.&lt;/p&gt;

&lt;p&gt;Prompting well is leading well. Scoping a task for AI is the same cognitive work as scoping a task for a junior engineer. The feedback loop is just faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Skills That Were Always There
&lt;/h2&gt;

&lt;p&gt;Soft skills were never actually soft. They were just undervalued because the market paid well for execution alone.&lt;/p&gt;

&lt;p&gt;Communication. Structured thinking. Prioritization. Breaking down complex problems. The ability to zoom out from the code and see the product. These were nice-to-haves when shipping code was hard. Now that shipping code is easy, they're the core competency.&lt;/p&gt;

&lt;p&gt;The developers who will thrive aren't necessarily the best at writing code. They're the best at knowing what code to write, why, and how to make sure it gets done right. Whether by themselves, a team, or a model.&lt;/p&gt;

&lt;p&gt;That's not a soft skill. That's the job now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do With This
&lt;/h2&gt;

&lt;p&gt;If you're a developer reading this, the question isn't whether AI will replace you. It's whether you're building the skills that AI can't replace.&lt;/p&gt;

&lt;p&gt;Start small: next time you open a chat with an AI, write the prompt as if you were briefing a capable but context-free junior engineer. Define the goal, the constraints, what done looks like, and what to avoid. See if the output changes.&lt;/p&gt;

&lt;p&gt;It will.&lt;/p&gt;

&lt;p&gt;That gap — between a poorly thought-out request and a well-structured one — is exactly where the value is now. Not just how you communicate it, but how clearly you've thought it through before you type anything.&lt;/p&gt;

&lt;p&gt;Starting there is the first step. But it goes deeper: spec-driven development, harness engineering, structured AI workflows, how teams are rethinking the entire dev process around these tools. That's what I'll keep writing about here.&lt;/p&gt;

&lt;p&gt;Subscribe if you want to follow along.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your 404 Logs Are a Security Report You're Ignoring</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:13:18 +0000</pubDate>
      <link>https://dev.to/edu_villao/your-404-logs-are-a-security-report-youre-ignoring-2c25</link>
      <guid>https://dev.to/edu_villao/your-404-logs-are-a-security-report-youre-ignoring-2c25</guid>
      <description>&lt;p&gt;Most WordPress developers install a redirect plugin, set up a few 301s, and never look at their 404 logs again. If they do, it's for SEO, fixing broken links, cleaning up crawl errors. That's the obvious use.&lt;/p&gt;

&lt;p&gt;But there's something else in those logs that almost everyone ignores.&lt;/p&gt;

&lt;p&gt;Your 404 logs are a real-time map of who's probing your site and what they're looking for. Bots scanning for exposed environment files, credential leaks, config files, path traversal exploits — all of it shows up as 404s before it shows up as a breach.&lt;/p&gt;

&lt;p&gt;I pulled the 404 logs from two of my own sites over the past week. Here's what I found, and what I blocked at the CDN level so these requests never hit my server again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;Two WordPress sites. One week of data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site A:&lt;/strong&gt; 1,388 total 404 requests across 1,118 unique URLs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site B:&lt;/strong&gt; 2,693 total 404 requests across 1,877 unique URLs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Over 4,000 requests that aren't real users, aren't search engines, and aren't broken links. They're automated scans looking for vulnerabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack Patterns
&lt;/h2&gt;

&lt;p&gt;Every 404 log tells a story. Here are the five categories I found in mine.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Environment File Scanning (.env)
&lt;/h3&gt;

&lt;p&gt;This is by far the most common pattern. Bots systematically scan every possible path where a &lt;code&gt;.env&lt;/code&gt; file might exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.env                    → 49 hits
/staging/.env            → 20 hits
/backend/.env            → 20 hits
/react/.env              → 15 hits
/.env.local              → 15 hits
/shared/.env             → 13 hits
/api/.env                → 12 hits
/.env.production         → 12 hits
/app/.env                → 10 hits
/server/.env             → 10 hits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it doesn't stop there. I found requests for &lt;code&gt;.env.backup&lt;/code&gt;, &lt;code&gt;.env.bak&lt;/code&gt;, &lt;code&gt;.env.save&lt;/code&gt;, &lt;code&gt;.env.old&lt;/code&gt;, &lt;code&gt;.env_copy&lt;/code&gt;, &lt;code&gt;.env_secret&lt;/code&gt;, &lt;code&gt;.env~&lt;/code&gt;, &lt;code&gt;.env.swp&lt;/code&gt; — over 80 different &lt;code&gt;.env&lt;/code&gt; variations in total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they're looking for:&lt;/strong&gt; Database credentials, API keys, SMTP passwords, Stripe keys, AWS secrets. One exposed &lt;code&gt;.env&lt;/code&gt; file = full access to your infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cloud Credential Harvesting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.aws/credentials        → 17 hits
/.aws/config             → 5 hits
/.AwS/CrEdEnTiAlS        → 5 hits (case variation to bypass rules)
/.terraform/terraform.tfstate → 6 hits
/serviceAccountKey.json  → 4 hits
/credentials.json        → 5 hits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the case variation on &lt;code&gt;.AwS/CrEdEnTiAlS&lt;/code&gt; — that's specifically designed to bypass naive pattern matching rules that only check lowercase. They're hunting for AWS keys, GCP service accounts, and Terraform state files that might contain infrastructure secrets.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Config &amp;amp; Secret File Probing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/config/initializers/secret_token.rb  → 7 hits (Rails)
/config/storage.yml                   → 6 hits (Rails)
/application.yml                      → 5 hits (Spring Boot)
/docker-compose.yml                   → 4 hits
/sftp-config.json                     → 5 hits (Sublime SFTP)
/.vscode/sftp.json                    → 3 hits (VS Code SFTP)
/config.php.bak                       → 5 hits
/secrets.json                         → 6 hits
/sendgrid.env                         → 7 hits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bots don't care what framework you use. They scan for Rails, Laravel, Spring Boot, Node.js, Django — all in the same sweep. If you accidentally deployed a config file, they'll find it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. WordPress-Specific Probing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/wp-config               → 3 hits
/wp-config~              → 1 hit
/wp-config.production    → 1 hit
/wp-configbak            → 1 hit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They're looking for backup copies of &lt;code&gt;wp-config.php&lt;/code&gt; — the file that contains your database credentials. Editor temp files (&lt;code&gt;wp-config~&lt;/code&gt;), manual backups (&lt;code&gt;wp-configbak&lt;/code&gt;), environment-specific copies. One careless &lt;code&gt;cp wp-config.php wp-config.bak&lt;/code&gt; and you've exposed everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Path Traversal &amp;amp; Code Execution Attempts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/passwd?raw??                                → 3 hits
/@fs/etc/passwd?import&amp;amp;raw??                     → 3 hits
/admin/config?cmd=cat /root/.aws/credentials     → 1 hit
/vendor/phpunit/phpunit/phpunit.xsd              → 2 hits
/pms?module=logging&amp;amp;file_name=../../~/.aws/credentials → 1 hit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are active exploitation attempts, not just scanning. They're trying to read system files, execute commands, and exploit known vulnerabilities in PHPUnit and other packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to Do About It: Block at the CDN
&lt;/h2&gt;

&lt;p&gt;Here's the key insight: every one of these requests hit your server. Your WordPress installation processed them, your PHP executed, your database was queried for a 404 page — all for a bot that's trying to hack you.&lt;/p&gt;

&lt;p&gt;The fix is simple: block these patterns at the CDN level (Cloudflare, Fastly, CloudFront...) so they never reach your server. Zero load on your infrastructure, zero PHP execution, zero risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare WAF Rules
&lt;/h2&gt;

&lt;p&gt;Here are the rules I set up based on my actual 404 data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; use &lt;code&gt;lower()&lt;/code&gt; in all of them — attackers use case variations like &lt;code&gt;.AwS/CrEdEnTiAlS&lt;/code&gt; to bypass naive rules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Rule 1: Block .env file access&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="py"&gt;Action:&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule 2: Block credential/config file access&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;".aws/credentials"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"terraform.tfstate"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"serviceaccountkey"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"docker-compose.yml"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"sftp-config"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"sftp.json"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"secrets.json"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"credentials.json"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"sendgrid"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"secret_token.rb"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"storage.yml"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"application.yml"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="py"&gt;Action:&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule 3: Block wp-config probing&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"wp-config"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;
 &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;http.request.uri.path&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/wp-admin/"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="py"&gt;Action:&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule 4: Block path traversal attempts&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"etc/passwd"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"phpunit"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"update.cgi"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="n"&gt;http.request.uri&lt;/span&gt; &lt;span class="ow"&gt;contains&lt;/span&gt; &lt;span class="s2"&gt;"cmd="&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="py"&gt;Action:&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule 5: Block common backup/install directory probing&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/old/"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/new/"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/backup/"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/wordpress/"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/wp/"&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
 &lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http.request.uri.path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s2"&gt;"/test/"&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="py"&gt;Action:&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloudflare string comparison is case-sensitive by default. The &lt;code&gt;lower()&lt;/code&gt; function converts the URI path to lowercase before comparison, so &lt;code&gt;/.AwS/CrEdEnTiAlS&lt;/code&gt; gets matched by a rule checking for &lt;code&gt;.aws/credentials&lt;/code&gt;. Without &lt;code&gt;lower()&lt;/code&gt;, that request slips through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CDN-Level Blocking Matters
&lt;/h2&gt;

&lt;p&gt;When you block at the CDN:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The request never reaches your server&lt;/li&gt;
&lt;li&gt;No PHP execution, no database query, no CPU usage&lt;/li&gt;
&lt;li&gt;Your server resources go to real users, not bots&lt;/li&gt;
&lt;li&gt;You reduce your attack surface to zero for known patterns&lt;/li&gt;
&lt;li&gt;Logs stay clean, making it easier to spot new patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Blocking at the application level (WordPress plugins, &lt;code&gt;.htaccess&lt;/code&gt;) still works, but the request already consumed server resources by the time it's blocked. CDN-level blocking is the difference between a bouncer at the door and a bouncer inside the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make This a Habit
&lt;/h2&gt;

&lt;p&gt;Pull your 404 logs once a month. Look for patterns. Add new Cloudflare rules.&lt;/p&gt;

&lt;p&gt;The bots evolve. New scanning patterns appear. The &lt;code&gt;.AwS/CrEdEnTiAlS&lt;/code&gt; case variation trick is a perfect example — someone specifically designed that to bypass lowercase-only rules.&lt;/p&gt;

&lt;p&gt;Your 404 logs aren't just broken links. They're a free security audit. Read them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The data in this post comes from real 404 logs collected over one week from two production WordPress sites. No IPs or identifying information from the attacking bots are included.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>wordpress</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Masky.js: A Lightweight Alternative to Inputmask, Cleave.js, and IMask</title>
      <dc:creator>Eduardo Villão</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:05:55 +0000</pubDate>
      <link>https://dev.to/edu_villao/maskyjs-a-lightweight-alternative-to-inputmask-cleavejs-and-imask-3pp5</link>
      <guid>https://dev.to/edu_villao/maskyjs-a-lightweight-alternative-to-inputmask-cleavejs-and-imask-3pp5</guid>
      <description>&lt;p&gt;Finding the right input masking library can be tricky. There are many options, each with its pros and cons. Some are feature-packed but heavy, while others are lightweight but miss critical functionalities like validation or mobile-friendly optimizations.&lt;/p&gt;

&lt;p&gt;In today's world, where performance and user experience are top priorities, selecting the right library is crucial. A solution that minimizes bundle size and enhances mobile usability while providing robust validation can make a huge difference in your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Popular Libraries and Their Limitations
&lt;/h2&gt;

&lt;p&gt;If you've needed input masking for forms, you've likely come across libraries like Inputmask, Cleave.js, and IMask. These libraries are really great, but they come with trade-offs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inputmask:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Powerful and flexible, but significantly increases bundle size (~20 KB gzipped).&lt;/li&gt;
&lt;li&gt;Lacks features like &lt;code&gt;inputmode&lt;/code&gt; for better mobile experiences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cleave.js:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Simple and supports dynamic masks.&lt;/li&gt;
&lt;li&gt;However, it lacks built-in validation or automatic configurations like &lt;code&gt;minlength&lt;/code&gt; and &lt;code&gt;maxlength&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;IMask:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Offers great features with a moderate size (~5 KB gzipped).&lt;/li&gt;
&lt;li&gt;It's powerful but can be overkill for simpler use cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While these are solid tools, I felt there was room for a solution that's lighter, flexible, and focused on mobile usability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Masky.js
&lt;/h2&gt;

&lt;p&gt;That's why I built &lt;strong&gt;Masky.js&lt;/strong&gt;: an ultra-lightweight (just 1.3 KB gzipped) alternative that prioritizes performance without sacrificing flexibility or essential features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Super Lightweight:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At just 1.3 KB gzipped, it's one of the smallest solutions on the market.&lt;/li&gt;
&lt;li&gt;Perfect for projects where bundle size is critical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mobile-Friendly:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically sets the &lt;code&gt;inputmode&lt;/code&gt; attribute based on the mask, ensuring a better typing experience on mobile devices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fully Customizable:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Supports custom masks with prefixes, suffixes, and even reverse masks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Built-In Validation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native support for CPF and CNPJ (Brazilian IDs) validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Automation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically calculates and applies &lt;code&gt;minlength&lt;/code&gt; and &lt;code&gt;maxlength&lt;/code&gt; based on the mask.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Zero Dependencies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100% Vanilla JS, making it easy to integrate with any environment or framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Masky.js&lt;/th&gt;
&lt;th&gt;Inputmask&lt;/th&gt;
&lt;th&gt;Cleave.js&lt;/th&gt;
&lt;th&gt;IMask&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Size (Gzipped)&lt;/td&gt;
&lt;td&gt;1.6 KB&lt;/td&gt;
&lt;td&gt;20 KB&lt;/td&gt;
&lt;td&gt;8 KB&lt;/td&gt;
&lt;td&gt;5 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Masks&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prefixes/Suffixes&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in CPF/CNPJ Validation&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;inputmode&lt;/code&gt; for Mobile&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automatic Min/Max Length&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse Masks&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Usage Example
&lt;/h2&gt;

&lt;p&gt;Simplicity is key. Just add the &lt;code&gt;data-mask&lt;/code&gt; attribute, and let Masky.js handle the rest — prefixes, suffixes, validations, and even automatic &lt;code&gt;inputmode&lt;/code&gt; and &lt;code&gt;minlength&lt;/code&gt; adjustments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Simple Phone Mask --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;data-mask=&lt;/span&gt;&lt;span class="s"&gt;"(00) 00000-0000"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Add Prefix and Suffix --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;data-mask=&lt;/span&gt;&lt;span class="s"&gt;"000-000"&lt;/span&gt; &lt;span class="na"&gt;data-mask-prefix=&lt;/span&gt;&lt;span class="s"&gt;"+55 "&lt;/span&gt; &lt;span class="na"&gt;data-mask-suffix=&lt;/span&gt;&lt;span class="s"&gt;" ext"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Built-in CPF Validation --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;data-mask=&lt;/span&gt;&lt;span class="s"&gt;"000.000.000-00"&lt;/span&gt; &lt;span class="na"&gt;data-mask-validation=&lt;/span&gt;&lt;span class="s"&gt;"cpf"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/masky-js/dist/masky.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check more details of how to use on the &lt;a href="https://github.com/eduardovillao/masky-js#readme" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it Out
&lt;/h2&gt;

&lt;p&gt;If you're looking for a fast, flexible, and performance-focused input masking solution, give Masky.js a shot!&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/eduardovillao/masky-js" rel="noopener noreferrer"&gt;https://github.com/eduardovillao/masky-js&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/masky-js" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/masky-js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear your thoughts or suggestions!&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
