Summary
Hey! Hi there, it's been a while 😁,
Thanks for clicking on this article 🙂, as always I hope you'll enjoy it.
In this back-to-school period, it might be a good idea to write another article, and besides, perfect timing, I have something interesting to tell you.
Yes, I never write articles if I think they don't have value, same goes for AI-generated articles, not on this blog, not my thing.
Though I'm not saying for other projects... 😝.
Today we're going to talk about Turnstile with Hotwired Turbo, are you ready? Let's go.
Currently, I'm working on redesigning ViteUneTable, remember? It's the application that uses Symfony UX Turbo, which I told you about in 2022.
As I was saying, this application uses Turbo and Stimulus, and it's been chugging along since 2022.
But now I need to modify it to add a "freemium" model, a free basic version with paid features.
And for that... I need to expose forms publicly, and, you know what happens to a public form? It gets spammed.
Bots are lurking and as soon as they see a form to fill on the web, they'll fill it with all sorts of not very interesting stuff.
In the history of the Internet, there have always been different methods to protect these forms, for example WordPress uses Akismet to protect comment forms.
There's also Google with ReCaptcha, very practical, it helps them train their image recognition models 😁, but all jokes aside, version 3 no longer asks users to click on images, it's quite transparent (but heavy for the browser).
And then, there's my favorite, Cloudflare Turnstile, it's simple, effective and it works, never had a problem with it, it gives them a bit of publicity but it doesn't bother me. Given the services they provide, a little publicity is fine, no problem.
Turbo is a library that aims to replace a classic web application, which on every link click, reloads all the CSS and JavaScript on the next page, with a "SPA" type application, which means "Single Page Application", with each interface change, the context doesn't reload entirely, only the HTML changes.
It's often accompanied by another library called "Stimulus" which handles more advanced animations.
These two libraries together, that's what we call the "Hotwire" stack.
Before we get to the code,
With Symfony, there's for example a bundle that allows you to integrate it into your forms, it works very well, you just have to add it to your form and it will load a template that embeds the Cloudflare Turnstile JavaScript.
The problem is that in our case, it's not a classic site, when we change pages, we don't reload the JavaScript, only the "body" and its HTML change. And the Cloudflare library doesn't detect that it should inject a Cloudflare "widget" for forms that have been injected into the DOM by Turbo.
Let's imagine we correctly load Turnstile on the page where there's a form, we leave this page because we change our mind, then we come back...
The problem? When we come back the widget will no longer be functional, the Turnstile JavaScript has already been loaded so it doesn't look for new forms on the page.
When with Turbo, you return to a page you've already visited (with the browser's back button or by clicking on the link again), Turbo will briefly fill your page with what it has in cache, namely the HTML in the state it was in when you left the page.
If you have something monitoring what's happening on the DOM, then you'll trigger the script twice, when Turbo's preview is applied, then when it has retrieved the real HTML from the server side.
From an optimization perspective it's horrible, because Turnstile performs tests in the browser, in other words if it does it twice in a row, we consume once for nothing.
In a battery-powered world, it's not the best idea.
Well, here I'm nitpicking a bit and it's unlikely to happen because who would put two forms with Turnstile on the same page (although, with modals).
But in the Turbo world, it's common to use a Stimulus controller for this kind of case.
And a Stimulus controller is linked to a DOM element, we could link it to a broader DOM element than just the "div" where we'll load the Turnstile widget.
But that wouldn't make our Stimulus controller easily portable to other projects (and therefore this article would be much less interesting).
Based on all the observations I just gave you, here's the code I set up to solve this problem.
You'll see that I use a lot of "statics", which is not common (and not very clean either) but here it allows me to set up portable code that handles "race conditions".
Here I use Symfony's Twig functions to declare the use of the Stimulus controller, but it would work just as well with simple data-attributes.
Nothing particularly special
And there you have it, our little back-to-school article comes to an end.
We're dealing with something quite simple to pick up but very useful, you'll soon see how I use it.
Also coming soon we'll leave the Symfony and Symfony-UX world for a bit to go to another stack, I won't tell you more, you'll see.
Have a great week.