Appearance
Cloudflare Turnstile
Cloudflare Turnstile is our CAPTCHA solution that replaced Google reCAPTCHA for better global performance and passive user experience.
Why Turnstile?
We migrated from Google reCAPTCHA to Cloudflare Turnstile for:
- Global accessibility: Better performance in regions where Google has known issues (e.g., China)
- Passive experience: Users are not actively challenged for proof of humanity under normal circumstances
Management
Dashboard Access
Turnstile configuration is managed at: Cloudflare Dashboard
There, we have two site keys:
- IxDF-web--sandbox (for development and staging)
- IxDF-web--production (for production)
Approved Domains
The following domains are configured:
interaction-design.org(production)ixdf.dev(development)staging.ixdf.dev(staging)localhost(local development)
Implementation
Package
We use ryangjchandler/laravel-cloudflare-turnstile, maintained by Ryan Chandler (Software Engineer at Laravel).
Configuration
Keys are configured in config/services.php:
php
'turnstile' => [
'key' => env('TURNSTILE_SITE_KEY', '0x4AAAAAABY_Hn_2YpySXd_5'),
'secret' => env('TURNSTILE_SECRET_KEY', '0x4AAAAAABY_HvuQ59JPdiZRHOkBoEk_Elc'),
],The key is used in the frontend (is public), while the secret is used in the backend to verify the response.
NOTE
You'll notice that our keys have default values — these are sandbox keys. We don't use them in production.
Usage Locations
Right now, turnstile is implemented on the following forms:
Contact forms:
- About contact:
/about/contact - Corporate contact:
/corporate - Affiliate contact:
/affiliate
- About contact:
Registration flows:
- Toolkits download:
/toolkits - Company signup:
/join/company/{slug}
- Toolkits download:
Authentication:
- Login form:
/login(conditional - see Rate Limiting docs)
- Login form:
Using in HTMX forms
When using HTMX, parts of the page are dynamically updated without a full page reload. This can cause issues with Turnstile widgets, as they need to be re-rendered after DOM changes.
The following code snippet handles this situation (see contactForm.js):
javascript
handleAfterSwap() {
// ...
// Re-render Turnstile after DOM changes
if (window.turnstile) {
const turnstileElement = this.querySelector('.cf-turnstile');
if (turnstileElement) {
window.turnstile.render(turnstileElement);
}
}
}