diff --git a/src/pages/api/aep-perso.ts b/src/pages/api/aep-perso.ts
new file mode 100644
index 0000000..d127226
--- /dev/null
+++ b/src/pages/api/aep-perso.ts
@@ -0,0 +1,196 @@
+import type { APIRoute } from 'astro'
+
+export const prerender = false
+
+const KIT_API_BASE = 'https://api.kit.com/v4'
+const TAG_NAME = 'AEP - sans perso'
+
+function htmlPage(title: string, heading: string, message: string): string {
+ return `
+
+
+
+
+ ${title} — trans-former.fr
+
+
+
+
+ ${heading}
+ ${message}
+
+ ← Retour sur trans-former.fr
+
+
+
+`
+}
+
+async function getSubscriberByEmail(email: string, apiKey: string): Promise {
+ const url = `${KIT_API_BASE}/subscribers?email_address=${encodeURIComponent(email)}`
+ const res = await fetch(url, {
+ headers: { 'X-Kit-Api-Key': apiKey },
+ signal: AbortSignal.timeout(10000),
+ })
+ if (!res.ok) return null
+ const data = await res.json()
+ if (!data?.subscribers?.length) return null
+ return String(data.subscribers[0].id)
+}
+
+async function getTagId(apiKey: string): Promise {
+ const url = `${KIT_API_BASE}/tags`
+ const res = await fetch(url, {
+ headers: { 'X-Kit-Api-Key': apiKey },
+ signal: AbortSignal.timeout(10000),
+ })
+ if (!res.ok) return null
+ const data = await res.json()
+ const tag = data?.tags?.find((t: any) => t.name === TAG_NAME)
+ return tag?.id ?? null
+}
+
+async function tagSubscriber(subscriberId: string, tagId: number, apiKey: string): Promise {
+ const url = `${KIT_API_BASE}/tags/${tagId}/subscribers`
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Kit-Api-Key': apiKey,
+ },
+ body: JSON.stringify({ id: subscriberId }),
+ signal: AbortSignal.timeout(10000),
+ })
+ return res.ok || res.status === 409
+}
+
+async function untagSubscriber(subscriberId: string, tagId: number, apiKey: string): Promise {
+ const url = `${KIT_API_BASE}/tags/${tagId}/subscribers/${subscriberId}`
+ const res = await fetch(url, {
+ method: 'DELETE',
+ headers: { 'X-Kit-Api-Key': apiKey },
+ signal: AbortSignal.timeout(10000),
+ })
+ return res.ok || res.status === 404
+}
+
+export const GET: APIRoute = async ({ url }) => {
+ const KIT_API_KEY = import.meta.env.KIT_API_SECRET_V4
+ if (!KIT_API_KEY) {
+ return new Response(htmlPage(
+ 'Erreur',
+ 'Configuration manquante',
+ 'Le service de désinscription n\'est pas configuré. Contacte-moi à julesneny8@gmail.com.'
+ ), {
+ status: 500,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+
+ const email = (url.searchParams.get('email') || '').trim().toLowerCase()
+ const action = url.searchParams.get('action')
+
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
+ return new Response(htmlPage(
+ 'Lien invalide',
+ 'Lien invalide',
+ 'Le lien que tu as suivi est incomplet ou mal formé. Essaye de copier-coller l\'URL complète depuis l\'email.'
+ ), {
+ status: 400,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+
+ if (action !== 'unsubscribe' && action !== 'resubscribe') {
+ return new Response(htmlPage(
+ 'Action inconnue',
+ 'Action inconnue',
+ 'Le lien que tu as suivi ne correspond à aucune action connue.'
+ ), {
+ status: 400,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+
+ try {
+ const subscriberId = await getSubscriberByEmail(email, KIT_API_KEY)
+ if (!subscriberId) {
+ return new Response(htmlPage(
+ 'Email inconnu',
+ 'Email inconnu',
+ 'Cet email n\'est pas abonné à la newsletter. Si tu penses qu\'il s\'agit d\'une erreur, écris-moi à julesneny8@gmail.com.'
+ ), {
+ status: 404,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+
+ const tagId = await getTagId(KIT_API_KEY)
+ if (!tagId) {
+ return new Response(htmlPage(
+ 'Erreur',
+ 'Configuration manquante',
+ 'Le tag de désinscription est introuvable. Contacte-moi à julesneny8@gmail.com.'
+ ), {
+ status: 500,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+
+ if (action === 'unsubscribe') {
+ const result = await tagSubscriber(subscriberId, tagId, KIT_API_KEY)
+ if (!result) {
+ return new Response(htmlPage(
+ 'Erreur',
+ 'Erreur technique',
+ 'Une erreur est survenue lors de la désinscription. Réessaie ou écris-moi à julesneny8@gmail.com.'
+ ), {
+ status: 500,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+ return new Response(htmlPage(
+ 'Désinscription réussie',
+ 'Tu es désinscrit·e',
+ 'Tu ne recevras plus les lettres « perso » de l\'infolettre Architecture d\'Écologie Politique. Tu continues à recevoir les textes de fond.
Si tu changes d\'avis, un lien de réabonnement est disponible dans chaque email de fond.'
+ ), {
+ status: 200,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+
+ if (action === 'resubscribe') {
+ const result = await untagSubscriber(subscriberId, tagId, KIT_API_KEY)
+ if (!result) {
+ return new Response(htmlPage(
+ 'Erreur',
+ 'Erreur technique',
+ 'Une erreur est survenue lors du réabonnement. Réessaie ou écris-moi à julesneny8@gmail.com.'
+ ), {
+ status: 500,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+ return new Response(htmlPage(
+ 'Réabonnement réussi',
+ 'Tu es de nouveau abonné·e',
+ 'Tu recevras à nouveau les lettres « perso » de l\'infolettre Architecture d\'Écologie Politique. Merci de ton intérêt !'
+ ), {
+ status: 200,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+
+ return new Response(null, { status: 400 })
+ } catch (e) {
+ return new Response(htmlPage(
+ 'Erreur',
+ 'Erreur technique',
+ 'Une erreur inattendue est survenue. Réessaie ou écris-moi à julesneny8@gmail.com.'
+ ), {
+ status: 500,
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
+ })
+ }
+}