Form Spam Protection 2.16 legacy 1.95
Every form submission (contact forms and event registrations) passes two independent checks. The honeypot always runs. reCAPTCHA runs on top of it as soon as keys are configured. Both checks are part of the package, a project only has to render the matching fields in its theme.
| Layer | Active | Catches |
|---|---|---|
| Honeypot field | always | bots that fill in every field |
| Honeypot timing gate | always | bots that post faster than a person can type |
| reCAPTCHA | when GOOGLE_RECAPTCHA_SECRET is set | bots that render the page and behave like a browser |
A submission that fails any check is rejected with a validation error. Nothing is stored and no mail goes out.
Honeypot
The form view renders two hidden inputs next to the CSRF token:
<input type="text" name="fax_number" value="" style="position: absolute; left: -9999px;" autocomplete="nope">
<input type="hidden" name="user_identification" value="{{ Crypt::encrypt(time()) }}">fax_numbermust arrive empty. People never see it, form filling bots usually do.user_identificationcarries the encrypted time the form was rendered. The submission must arrive at leastevolve.honeypot-thresholdseconds later (default 3). A missing or tampered value fails validation.
'honeypot-threshold' => env('EVOLVE_HONEYPOT_THRESHOLD', 3),Projects that still set the older key core.honeypot-threshold keep their value, it takes precedence. Before 2.16 (legacy 1.95) the gate read only that older key, which no config defined, so it caught nothing but submissions within the same second.
With the page cache active the theme refreshes user_identification on every page load through /api/csrf-token, so cached pages do not carry a stale timestamp.
reCAPTCHA
Which key type
Google issues three kinds of reCAPTCHA keys. The key type is chosen when the key is created in the reCAPTCHA admin console and cannot be changed later.
| Key type | Works with the Evolve theme | Server check |
|---|---|---|
| v3 (score based) | yes, recommended | success and score ≥ threshold |
| v2 Invisible | yes | success only, no score |
| v2 Checkbox ("I'm not a robot") | no |
The theme binds reCAPTCHA to the submit button (class="g-recaptcha" with data-callback). That binding works for v3 and v2 Invisible keys. A v2 Checkbox key needs a visible widget, which the theme does not render. Visitors see no challenge with either supported type.
Each key is bound to a list of domains. Add the customer's domain (and the staging domain, if it should run there too) to the key in the console. A key used on a domain that is not listed fails every submission.
Configuration
Set the keys in the panel under Settings > Google or in the .env:
GOOGLE_RECAPTCHA_SITEKEY=6Lc...
GOOGLE_RECAPTCHA_SECRET=6Lc...
GOOGLE_RECAPTCHA_SCORE=0.5'google' => [
'recaptcha' => [
'secret' => env('GOOGLE_RECAPTCHA_SECRET'),
'site-key' => env('GOOGLE_RECAPTCHA_SITEKEY'),
'score' => env('GOOGLE_RECAPTCHA_SCORE', 0.5),
],
],- Site key switches the theme to the reCAPTCHA submit and makes the head load
recaptcha/api.js. - Secret switches the server check on. From then on every submission needs a valid
g-recaptcha-responsetoken. - Score is the minimum a v3 token needs, from 0.0 (bot) to 1.0 (human). 0.5 is Google's suggested default. Lower it if real enquiries bounce, raise it if spam still gets through. Values outside 0 to 1 are clamped, anything non numeric falls back to 0.5.
Set site key and secret together. A secret without a site key rejects every submission, because the theme never sends a token.
If Google cannot be reached, the submission is rejected. Before 2.16 (legacy 1.95) the score was not checked at all: any token Google accepted passed, including those scored as bots.
Legacy projects with their own google block
Legacy 1.x merges config/services.php shallowly. A project that defines the whole google key in its published config/services.php loses the package's score key. The check then uses 0.5, but GOOGLE_RECAPTCHA_SCORE and the settings field have no effect until the project adds the score line shown above.
Theme integration
The starter theme ships with it. Projects with their own components/form.blade.php need the button switch and a callback that submits the form:
@php
$recaptchaCallback = 'submitForm' . preg_replace('/[^A-Za-z0-9]/', '', $form->name);
@endphp
<form id="form{{ $form->name }}" ...>
...
@if(config('services.google.recaptcha.site-key'))
<button
class="g-recaptcha btn btn-primary"
data-sitekey="{{ config('services.google.recaptcha.site-key') }}"
data-callback="{{ $recaptchaCallback }}"
data-action="submit">
{{ __('evolve::messages.send') }}
</button>
@else
<button type="submit" class="btn btn-primary">{{ __('evolve::messages.send') }}</button>
@endif
</form>
@if(config('services.google.recaptcha.site-key'))
<script>
function {{ $recaptchaCallback }}(token) {
const form = document.getElementById(@js('form' . $form->name));
let input = form.querySelector('input[name="g-recaptcha-response"]');
if (! input) {
input = Object.assign(document.createElement('input'), { type: 'hidden', name: 'g-recaptcha-response' });
form.appendChild(input);
}
input.value = token;
form.submit();
}
</script>
@endifPoints to watch:
- The
idbelongs on the<form>only. Older project themes putid="form…"on the wrapping<div>as well.getElementByIdthen returns the div andsubmit()fails silently. - The callback name must be a valid JavaScript identifier. Form names may contain hyphens, hence the
preg_replace. api.jsmust be on the page. The Evolve theme loads it inhead.blade.phpwhen a site key is set. Projects with their own head (most legacy themes) do not. Without it the button submits nothing and nothing happens. Load it once in the form component:
@once
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
@endonce- Show spam failures. The spam fields have no visible input, so their errors are not rendered next to a field. Without a message the visitor only sees the form again:
@if($errors->hasAny(['g-recaptcha-response', 'user_identification', 'fax_number']))
<div class="text-sm text-red-500">{{ __('Your request could not be verified. Please try again.') }}</div>
@endifPrivacy
reCAPTCHA sends visitor data to Google. Mentioning it in the site's privacy policy is the customer's responsibility.
Environment variables
| Variable | Default | Effect |
|---|---|---|
EVOLVE_HONEYPOT_THRESHOLD | 3 | Seconds a form has to be open before a submission is accepted. |
GOOGLE_RECAPTCHA_SITEKEY | unset | Public key, switches the theme to the reCAPTCHA submit. Settings panel. |
GOOGLE_RECAPTCHA_SECRET | unset | Secret key, switches the server check on. Settings panel. |
GOOGLE_RECAPTCHA_SCORE | 0.5 | Minimum v3 score. Settings panel. |
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Every submission fails after adding keys | Key not valid for this domain, v2 Checkbox key, or only the secret set |
| Real enquiries bounce with the verification message | Score too strict, lower GOOGLE_RECAPTCHA_SCORE |
| Nothing happens on submit | Duplicate id="form…" in the theme, or api.js not loaded (site key missing, or a project head that does not include it) |
| Spam still arrives | No secret set (only the honeypot runs), or a v2 Invisible key (no score to check) |