diff --git a/.gitignore b/.gitignore
index 510df57..a0f7b25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,11 @@ Icon
/site/config/.license
+# Host-specific config (credentials)
+# ---------------
+
+/site/config/config.index.ngo.php
+
# Content
# ---------------
diff --git a/assets/js/newsletter-brevo.js b/assets/js/newsletter-brevo.js
new file mode 100644
index 0000000..67b1b91
--- /dev/null
+++ b/assets/js/newsletter-brevo.js
@@ -0,0 +1,89 @@
+(function () {
+ 'use strict';
+
+ const PROXY_URL = '/api/newsletter';
+
+ async function subscribeToNewsletter(email, attributes = {}) {
+ const response = await fetch(PROXY_URL, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email, attributes }),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ const error = new Error(
+ data.user_message || data.message || 'Subscription error'
+ );
+ error.code = data.error;
+ error.data = data;
+ throw error;
+ }
+
+ return data;
+ }
+
+ function showMessage(form, text, isError = false) {
+ const oldMessages = form.parentNode.querySelectorAll('.newsletter-message');
+ oldMessages.forEach((msg) => msg.remove());
+
+ const message = document.createElement('p');
+ message.className = 'newsletter-message';
+ message.textContent = text;
+ message.style.marginTop = '0.5rem';
+ message.style.fontSize = '0.9rem';
+ message.style.color = isError
+ ? 'var(--color-error, #ef4444)'
+ : 'var(--color-success, #22c55e)';
+
+ form.parentNode.insertBefore(message, form.nextSibling);
+
+ if (!isError) {
+ setTimeout(() => message.remove(), 5000);
+ }
+ }
+
+ async function handleFormSubmit(event) {
+ event.preventDefault();
+
+ const form = event.target;
+ const emailInput = form.querySelector('input[type="email"]');
+ const submitButton = form.querySelector('button[type="submit"]');
+
+ if (!emailInput || !emailInput.value) {
+ const message = document.documentElement.lang.startsWith('en')
+ ? 'Please enter a valid email address.'
+ : 'Veuillez entrer une adresse email valide.';
+ showMessage(form, message, true);
+ return;
+ }
+
+ const email = emailInput.value.trim();
+ submitButton.disabled = true;
+
+ try {
+ await subscribeToNewsletter(email);
+ const message = document.documentElement.lang.startsWith('en')
+ ? 'Thank you! Your subscription has been confirmed.'
+ : 'Merci, votre inscription est confirmée !';
+ showMessage(form, message, false);
+ form.reset();
+ } catch (error) {
+ const isAlreadySubscribed = error.code === 'email_already_exists';
+ showMessage(form, error.message, !isAlreadySubscribed);
+ } finally {
+ submitButton.disabled = false;
+ }
+ }
+
+ function initNewsletterForms() {
+ const forms = document.querySelectorAll('.form__newsletter');
+ forms.forEach((form) => form.addEventListener('submit', handleFormSubmit));
+ }
+
+ document.addEventListener('DOMContentLoaded', initNewsletterForms);
+})();
diff --git a/site/config/config.php b/site/config/config.php
index b55f626..766c527 100644
--- a/site/config/config.php
+++ b/site/config/config.php
@@ -16,7 +16,7 @@ return [
'default' => [
'width' => 1024, 'format' => 'webp'
],
- 'full' => 2048,
+ 'full' => 2048,
'format' => 'webp'
],
'srcsets' => [
@@ -76,6 +76,10 @@ return [
],
'tobimori.seo.canonicalBase' => 'https://www.index.ngo',
+ 'routes' => [
+ require(__DIR__ . '/routes/newsletter.php'),
+ ],
+
'hooks' => [
'page.update:after' => function ($newPage) {
if ($newPage->intendedTemplate()->name() !== 'investigation') {
@@ -112,4 +116,4 @@ return [
}
}
]
-];
\ No newline at end of file
+];
diff --git a/site/config/routes/newsletter.php b/site/config/routes/newsletter.php
new file mode 100644
index 0000000..577ed2d
--- /dev/null
+++ b/site/config/routes/newsletter.php
@@ -0,0 +1,116 @@
+ 'api/newsletter',
+ 'method' => 'POST|OPTIONS',
+ 'action' => function () {
+ header('Content-Type: application/json');
+
+ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
+ http_response_code(200);
+ die();
+ }
+
+ $config = kirby()->option('brevo');
+ $apiKey = $config['api_key'] ?? '';
+ $listId = (int)($config['list_id'] ?? 2);
+ $apiUrl = $config['api_url'] ?? 'https://api.brevo.com/v3/contacts';
+
+ if (empty($apiKey)) {
+ http_response_code(500);
+ die(json_encode(['error' => 'Server configuration error', 'message' => 'Brevo API key not configured']));
+ }
+
+ $input = file_get_contents('php://input');
+ $data = json_decode($input, true);
+
+ if (!isset($data['email']) || empty($data['email'])) {
+ http_response_code(400);
+ die(json_encode(['error' => 'Email required']));
+ }
+
+ $email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
+ if ($email === false) {
+ http_response_code(400);
+ die(json_encode(['error' => 'Invalid email']));
+ }
+
+ $brevoData = [
+ 'email' => $email,
+ 'listIds' => [$listId],
+ 'updateEnabled' => true,
+ ];
+
+ if (isset($data['attributes']) && is_array($data['attributes']) && !empty($data['attributes'])) {
+ $brevoData['attributes'] = $data['attributes'];
+ }
+
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $apiUrl,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => json_encode($brevoData),
+ CURLOPT_HTTPHEADER => [
+ 'Content-Type: application/json',
+ 'api-key: ' . $apiKey,
+ 'User-Agent: Index-NGO-Newsletter',
+ ],
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_SSL_VERIFYPEER => true,
+ ]);
+
+ $response = curl_exec($ch);
+ $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ $curlError = curl_error($ch);
+ curl_close($ch);
+
+ if ($response === false) {
+ http_response_code(500);
+ die(json_encode(['error' => 'Connection error', 'details' => $curlError]));
+ }
+
+ $responseData = json_decode($response, true);
+
+ switch ($httpCode) {
+ case 201:
+ case 204:
+ http_response_code(200);
+ die(json_encode(['success' => true, 'message' => 'Successfully subscribed', 'email' => $email]));
+
+ case 400:
+ $isDuplicate = isset($responseData['code']) && $responseData['code'] === 'duplicate_parameter';
+ http_response_code(400);
+ die(json_encode([
+ 'error' => $isDuplicate ? 'email_already_exists' : 'invalid_data',
+ 'message' => $isDuplicate ? 'You are already subscribed!' : 'Invalid email address.',
+ 'user_message' => $isDuplicate ? 'Vous êtes déjà inscrit·e !' : 'Veuillez vérifier votre adresse email.',
+ ]));
+
+ case 401:
+ http_response_code(500);
+ die(json_encode([
+ 'error' => 'invalid_api_key',
+ 'message' => 'Invalid API key',
+ 'user_message' => 'Une erreur technique est survenue. Veuillez réessayer plus tard.',
+ ]));
+
+ case 404:
+ http_response_code(500);
+ die(json_encode([
+ 'error' => 'list_not_found',
+ 'message' => 'Contact list not found',
+ 'user_message' => 'Une erreur technique est survenue. Veuillez réessayer plus tard.',
+ ]));
+
+ default:
+ http_response_code($httpCode);
+ die(json_encode([
+ 'error' => 'api_error',
+ 'message' => 'Error communicating with subscription service',
+ 'user_message' => 'Une erreur est survenue. Veuillez réessayer.',
+ 'http_code' => $httpCode,
+ ]));
+ }
+ },
+];
diff --git a/site/templates/newsletter.php b/site/templates/newsletter.php
index 0e0ab55..e9c1ec8 100644
--- a/site/templates/newsletter.php
+++ b/site/templates/newsletter.php
@@ -1,4 +1,5 @@
+