# Simple CAPTCHA

Source: https://kart.bnomei.com/docs/forms/simple-captcha
Updated: 2025-08-08T14:05:47+00:00
Summary: Add Simple Captcha to Kirby CMS forms. Configure image style, use snippet or Alpine.js refresh, and secure login, signup, and magic-link routes with CSRF.

## Image

To protect your forms from bots, you can require users to fill out a [Simple Captcha](https://github.com/S1SYPHOS/php-simple-captcha) text as seen on a dynamically created image.

## Configuration

You need to explicitly enable the CAPTCHA first, and you might want to change how the CAPTCHA looks. This can be done with the `captcha.set` config option.

Path: /site/config/config.php  
Code (php):  
```
<?php

return [
    // enable the captcha check and its API endpoints
    'bnomei.kart.captcha.enabled' => true,

    // configure how the generated image looks like
    'bnomei.kart.captcha.set' => function () {
        // https://github.com/S1SYPHOS/php-simple-captcha
        $builder = new \Bnomei\Kart\CaptchaBuilder;
        $builder->bgColor = '#FFFFFF';
        $builder->lineColor = '#FFFFFF';
        $builder->textColor = '#000000';
        $builder->applyEffects = false;
        $builder->build();
        kirby()->session()->set('captcha', $builder->phrase);

        return [
            'captcha' => $builder->inline(),
        ];
    },
    // other options...
];
```

  
## Securing Forms

The plugin does provide a snippet to render the image.

Code (plaintext):  
```
<?php snippet('kart/captcha') ?>
```

  
However, the following example shows how to use [Alpine.js](https://alpinejs.dev/) to render the image dynamically, allowing for manual refreshes.

Code (php):  
```
<form method="POST" action="<?= kart()->urls()->login() ?>">
    <label>
        <input type="email" name="email" required
                  placeholder="<?= t('email') ?>" autocomplete="email"
                  value="<?= urldecode(get('email', '')) ?>">
    </label>
    <label>
        <input type="password" name="password" required
               placeholder="<?= t('password') ?>" autocomplete="off">
    </label>
    <div x-data="{
    captcha: undefined,
    refresh() {
      fetch('<?= kart()->urls()->captcha() ?>')
        .then(response => response.json())
        .then(data => {
          this.captcha = data.captcha;
        })
    }
  }" x-init="refresh()">
        <label>
            <span>Captcha: </span>
            <input name="captcha" type="text" value=""
                   required pattern="[a-zA-Z0-9]{5}">
        </label>
        <figure>
            <img :src="captcha" width="150" height="40"/>
        </figure>
        <button x-on:click="refresh()" type="button">Refresh</button>
    </div>
    <input type="hidden" name="redirect" value="<?= $page?->url() ?>">
    <button type="submit" onclick="this.disabled=true;this.form.submit();"><?= t('login') ?></button>
</form>
```

  
## Behind the Scenes

The endpoints intended for public use, which are most likely targeted by bots (login, register/signup, magic-link), are preconfigured with a check for the Simple Captcha.

If posted with the form, the `\Bnomei\Kart\Router::denied()`-helper will query the current request for the `captcha` and check whether the form is legit.

Even if you use Captcha, you should keep the `CSRF`-check as an additional layer of security.

If the form is successful, it will continue as intended. If not, it will redirect to the error page or return the respective HTTP status code.