Branche le formulaire de contact sur Resend
Deploy to VPS / deploy (push) Has been cancelled

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 08:38:56 +02:00
parent 0c88206a23
commit 0cd61f2a12
7 changed files with 121 additions and 46 deletions
+14
View File
@@ -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
+1
View File
@@ -32,6 +32,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
# vercel
.vercel
+7 -1
View File
@@ -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).
+6 -3
View File
@@ -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`)
+40 -3
View File
@@ -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",
+51 -38
View File
@@ -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"
}
+2 -1
View File
@@ -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",