From 0cd61f2a12fb21126859cd55185e353fbe5dc7f9 Mon Sep 17 00:00:00 2001 From: Mathew Date: Tue, 23 Jun 2026 08:38:56 +0200 Subject: [PATCH] Branche le formulaire de contact sur Resend - Envoi reel via le SDK Resend dans app/actions/contact.ts (avec gestion d'erreur) - Cles/adresses lues depuis l'env (RESEND_API_KEY, CONTACT_TO_EMAIL, CONTACT_FROM_EMAIL) - Ajout de resend en dependance + lock synchronise - .env.example documente la config (autorise dans .gitignore) - README + TODO a jour Co-Authored-By: Claude Opus 4.8 --- .env.example | 14 +++++++ .gitignore | 1 + README.md | 8 +++- TODO.md | 9 +++-- app/actions/contact.ts | 43 ++++++++++++++++++-- package-lock.json | 89 ++++++++++++++++++++++++------------------ package.json | 3 +- 7 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a488851 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Copy this file to `.env.local` for local dev (or set these in your host's env in prod). +# Never commit real values — `.env*` is gitignored. + +# Public site URL (used for canonical URLs, OG metadata, etc.) +NEXT_PUBLIC_SITE_URL=https://highlandgamesstudio.com + +# --- Contact form (Resend) --- +# API key from https://resend.com/api-keys +RESEND_API_KEY= +# Address that RECEIVES contact messages +CONTACT_TO_EMAIL=hello@highlandgamesstudio.com +# Sender address — must be on a domain verified in Resend. +# Use onboarding@resend.dev for testing (only delivers to your own account email). +CONTACT_FROM_EMAIL=onboarding@resend.dev diff --git a/.gitignore b/.gitignore index 5ef6a52..7b8da95 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel diff --git a/README.md b/README.md index b1a4e54..4cf0ec1 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,14 @@ scripts/ One-off build scripts (favicon generation) ## Configuration +Copy `.env.example` to `.env.local` and fill in the values. + - **Site URL** — set `NEXT_PUBLIC_SITE_URL` per environment (used by metadata, sitemap, robots, RSS and JSON-LD). Defaults to `https://highlandgamesstudio.com` — see `lib/site.ts`. +- **Contact form (Resend)** — the contact form sends email via [Resend](https://resend.com). + Set `RESEND_API_KEY`, `CONTACT_TO_EMAIL` (who receives messages) and `CONTACT_FROM_EMAIL` + (sender, must be on a domain verified in Resend). Without these, the form returns a + friendly error instead of sending. See `app/actions/contact.ts`. ## Adding a devblog post @@ -70,5 +76,5 @@ PM2 process. Requires the `SSH_*` repository secrets. ## Known TODOs -- Wire the contact and newsletter forms to a real email provider (currently log-only). +- Wire the newsletter form to a real email provider (contact form now uses Resend). - Replace placeholder content (game, team members, social links). diff --git a/TODO.md b/TODO.md index 453cb59..1b21a49 100644 --- a/TODO.md +++ b/TODO.md @@ -14,10 +14,13 @@ Dernière mise à jour : 2026-06-22 - [ ] Idéalement : servir le fichier hors-Git (CDN / objet statique) - [ ] **Formulaires non fonctionnels** (perdent les données, juste un `console.log`) - - [ ] Choisir un service email (Resend / Postmark / Buttondown…) - - [ ] Brancher le formulaire de contact → `app/actions/contact.ts` + - [x] Choisir un service email → **Resend** + - [x] Brancher le formulaire de contact → `app/actions/contact.ts` - [ ] Brancher la newsletter → `app/actions/subscribe.ts` - - [ ] Stocker la clé API dans une variable d'env (`.env`) + - [x] Clé API + adresses lues depuis l'env (`.env.example` créé) + - [ ] **À faire côté toi** : `npm install resend`, créer un compte Resend, + vérifier le domaine, et renseigner `RESEND_API_KEY` / `CONTACT_TO_EMAIL` + / `CONTACT_FROM_EMAIL` (en local dans `.env.local` + en prod sur le VPS) - [ ] **Déploiement à valider en réel** - [x] Trigger workflow corrigé (`main` → `master`) diff --git a/app/actions/contact.ts b/app/actions/contact.ts index 0733870..d9afb8a 100644 --- a/app/actions/contact.ts +++ b/app/actions/contact.ts @@ -1,5 +1,7 @@ "use server"; +import { Resend } from "resend"; + export type ContactState = { status: "idle" | "success" | "error"; message: string; @@ -38,9 +40,44 @@ export async function sendContact( }; } - // TODO: wire to Resend / Postmark / SendGrid to actually deliver the email. - // For now we log so messages are visible in server logs. - console.log("[contact]", { name, email, subject, message }); + const apiKey = process.env.RESEND_API_KEY; + const to = process.env.CONTACT_TO_EMAIL; + const from = process.env.CONTACT_FROM_EMAIL; + + if (!apiKey || !to || !from) { + console.error( + "[contact] Missing email env vars (RESEND_API_KEY / CONTACT_TO_EMAIL / CONTACT_FROM_EMAIL).", + ); + return { + status: "error", + message: "Email isn't configured yet. Please try again later.", + }; + } + + try { + const resend = new Resend(apiKey); + const { error } = await resend.emails.send({ + from, + to, + replyTo: email, + subject: `[Contact] ${subject}`, + text: `From: ${name} <${email}>\n\n${message}`, + }); + + if (error) { + console.error("[contact] Resend error:", error); + return { + status: "error", + message: "Something went wrong sending your message. Please try again.", + }; + } + } catch (err) { + console.error("[contact] Unexpected error:", err); + return { + status: "error", + message: "Something went wrong sending your message. Please try again.", + }; + } return { status: "success", diff --git a/package-lock.json b/package-lock.json index 9c0cd2d..b2c3309 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "next-mdx-remote": "^6.0.0", "react": "19.2.4", "react-dom": "19.2.4", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "resend": "^6.14.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -71,7 +72,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -296,7 +296,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -541,7 +540,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -564,7 +562,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -587,7 +584,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -604,7 +600,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -621,7 +616,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -638,7 +632,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -655,7 +648,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -672,7 +664,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -689,7 +680,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -706,7 +696,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -723,7 +712,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -740,7 +728,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -757,7 +744,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -780,7 +766,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -803,7 +788,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -826,7 +810,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -849,7 +832,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -872,7 +854,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -895,7 +876,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -918,7 +898,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -941,7 +920,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -961,7 +939,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -981,7 +958,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1001,7 +977,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -1330,6 +1305,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1704,7 +1685,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1770,7 +1750,6 @@ "integrity": "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", @@ -2301,7 +2280,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2663,7 +2641,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3366,7 +3343,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3552,7 +3528,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -3941,6 +3916,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -7081,6 +7062,12 @@ "node": ">= 0.4" } }, + "node_modules/postal-mime": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", + "license": "MIT-0" + }, "node_modules/postcss": { "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", @@ -7178,7 +7165,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7188,7 +7174,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7409,6 +7394,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/resend": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.14.0.tgz", + "integrity": "sha512-jVdpUgOoWGLjaP64lo8KwzHT9gY4w6Dl8c36CIb2F+ayYOMLr3khqs8xrNjXM2k19b+lPoj0VWQFhVNLiToBjA==", + "license": "MIT", + "dependencies": { + "postal-mime": "2.7.4", + "standardwebhooks": "1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-email/render": "*" + }, + "peerDependenciesMeta": { + "@react-email/render": { + "optional": true + } + } + }, "node_modules/resolve": { "version": "2.0.0-next.6", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", @@ -7819,6 +7825,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -8121,7 +8137,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8304,7 +8319,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8752,7 +8766,6 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 498816b..b98cab4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "next-mdx-remote": "^6.0.0", "react": "19.2.4", "react-dom": "19.2.4", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "resend": "^6.14.0" }, "devDependencies": { "@tailwindcss/postcss": "^4",