{% extends layout %} {% block title %}Contact{% endblock %} {% block description %}Get in touch — send a message using the contact form.{% endblock %} {# Every block below is optional — the layout already supplies a sensible default for each (see /admin/docs/seo). Shown here uncommented as a reference for every override a page can make; delete what you don't need. #} {% block robots %}index, follow{% endblock %} {% block canonical %}{{ request_path|default('/') }}{% endblock %} {% block og_type %}website{% endblock %} {% block og_title %}{{ block('title') }}{% endblock %} {% block og_description %}{{ block('description') }}{% endblock %} {% block og_url %}{{ block('canonical') }}{% endblock %} {% block twitter_card %}summary{% endblock %} {% block twitter_title %}{{ block('title') }}{% endblock %} {% block twitter_description %}{{ block('description') }}{% endblock %} {% import '_layout/icons.twig' as icons %} {% block content %}

{{ icons.email() }}Contact

This form is handled entirely by App/pages/contact/index.php, a sidecar demonstrating the classic POST/redirect/GET pattern: it validates $_POST, calls Lib\Mailer::send() on success, and redirects to /contact?sent=1 — so refreshing the page after submitting never resubmits the form. Because this page has a sidecar, it's never served from the static cache; see {{ icons.book() }}Sidecars and {{ icons.book() }}Libraries for the full contract. It's also a working example of self-hosted spam prevention (honeypot field + submission-timing check, no external CAPTCHA service) — see {{ icons.book() }}Sidecars' "Spam prevention" section.

{% if sent %}

Thanks — your message has been sent.

{% endif %} {% if securityError %}

Your session expired before submitting — please try again.

{% endif %}


{% if errors.name %}
{{ errors.name }}{% endif %}


{% if errors.email %}
{{ errors.email }}{% endif %}


{% if errors.message %}
{{ errors.message }}{% endif %}

{% endblock %}