Пора заканчивать эпопею про избавление от Js. Чтобы не утомлять, расскажу о последнем штрихе — как внедрил капчу для комментариев.

Как только я убрал Disqus, полезли спамные комментарии. Каждый день приходят два-три предложения купить виагру, надувную лодку или просто левые ссылки. Поскольку каждый комментарий открывает PR в репозиторий, все остается в истории Гитхаба. Посмотреть на это добро можно по ссылке.

Разгребать подобные комментарии нет желания, поэтому должна быть минимальная защита от спама. С условием — без Js. Надумал такую схему:

  • капча генерируется на этапе сборки блога. На выходе получается HTML-форма с полем captcha и значением 2 × 5.
  • В форму добавляется поле для решения.
  • Сервер парсит капчу, решает и сверяет с ответом. Если что-то не так, заворачивает комментарий.

Как ни странно, даже на таком примитиве боты отваливаются. Разве что с оговоркой: когда был оператор +, боты решали капчу. Как только заменил на × (знак умножения в юникоде), стала тишь да благодать. Надеюсь, читатель не забыл таблицу умножения! Тестируя форму, сам подвис с примером 8 × 9.

Техническая сторона: вот построить капчу в шаблоне:


{% assign val1 = '1 2 3 4 5 6 7 8 9' | split: ' ' | sample %}
{% assign val2 = '1 2 3 4 5 6 7 8 9' | split: ' ' | sample %}
{% assign op   = '×'            | split: ' ' | sample %}
{% assign captcha = val1 | append: " " | append: op | append: " " | append: val2 %}

Замечу, что при каждой сборке блога значения будут разные.

Скрытое поле в форме:


<input required name="captcha" type="hidden" value="{{ captcha }}">

Виджет для ввода решения:


<div class="block">
    <span class="comment-form-label"><small>{{ captcha }} = </small></span>
    <input required id="comment-form-solution" name="solution" type="text">
</div>

Наконец, серверный код проверки капчи:

(dеfn validate-captcha [captcha solution]
  (when-let [[_ val1-raw op-raw val2-raw]
             (re-find #"^(-?\d+) (.+?) (-?\d+)$" captcha)]

    (let [val1
          (Integer/parseInt val1-raw)

          val2
          (Integer/parseInt val2-raw)

          op
          (case op-raw
            ("+" "&#43;") +
            ("*" "×" "&#215;") *
            nil)]

      (when (and val1 val2 op)
        (= (str (op val1 val2))
           (str/trim solution))))))

Грубо, неуклюже, но работает.

На этом я закончу тему с избавлением от Js. На мой взгляд, цели достигнуты, жить с новыми комментариями можно. Это был интересный опыт, в будущем пригодится.