Skip to content

Commit fc124a6

Browse files
HugoRCDatinux
andauthored
feat(newsletter): migrate from sendgrid to resend (#2181)
Co-authored-by: Sébastien Chopin <atinux@gmail.com>
1 parent 2335451 commit fc124a6

8 files changed

Lines changed: 107 additions & 195 deletions

File tree

.env.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ MCP_URL=http://localhost:3000/mcp
3434
# for fetching sponsors
3535
NUXT_OPEN_COLLECTIVE_API_KEY=
3636

37-
# for sending emails via SendGrid
38-
NUXT_SENDGRID_API_KEY=
39-
NUXT_SENDGRID_LIST_ID=
37+
# for newsletter subscription via Resend
38+
NUXT_RESEND_API_KEY=
39+
NUXT_RESEND_AUDIENCE_ID=
4040

4141
# for overriding the contact email used in the site footer and contact forms
4242
NUXT_CONTACT_EMAIL=""

nuxt.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ export default defineNuxtConfig({
101101
openCollective: {
102102
apiKey: ''
103103
},
104-
sendgrid: {
105-
listId: '',
106-
apiKey: ''
104+
resend: {
105+
apiKey: '',
106+
audienceId: ''
107107
}
108108
},
109109
routeRules: {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"nuxt-llms": "^0.2.0",
6161
"nuxt-og-image": "^5.1.13",
6262
"ofetch": "^1.5.1",
63+
"resend": "^6.9.2",
6364
"scule": "^1.3.0",
6465
"sitemap": "^9.0.0",
6566
"std-env": "^3.10.0",

pnpm-lock.yaml

Lines changed: 55 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/api/newsletter/confirm.post.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,38 @@
1+
import { Resend } from 'resend'
12
import { z } from 'zod'
23

34
export default eventHandler(async (event) => {
4-
// Get the email from body
55
const { email, confirmation } = await readValidatedBody(event, z.object({
66
email: z.string().email().trim(),
77
confirmation: z.string()
88
}).parse)
99

10-
const listId = useRuntimeConfig(event).sendgrid.listId
11-
if (!listId) {
10+
const { apiKey, audienceId } = useRuntimeConfig(event).resend
11+
if (!apiKey || !audienceId) {
1212
throw createError({
1313
statusCode: 500,
14-
message: 'Missing NUXT_SENDGRID_LIST_ID env variable'
14+
message: 'Missing Resend configuration'
1515
})
1616
}
1717

18-
// Validate confirmation code
1918
if (generateConfirmation(event, email) !== confirmation) {
2019
throw createError({
2120
statusCode: 400,
2221
message: 'Confirmation code is invalid.'
2322
})
2423
}
2524

26-
// Add to contacts list
27-
try {
28-
await sendgrid.addContactToList(event, email, listId)
29-
} catch (_err) {
30-
const err = _err as Error & { response?: { body?: { errors?: Array<{ message?: string }> } } }
25+
const resend = new Resend(apiKey)
26+
27+
const { error } = await resend.contacts.create({
28+
email,
29+
audienceId
30+
})
31+
32+
if (error) {
3133
throw createError({
32-
message: err?.response?.body?.errors?.[0]?.message || 'Invalid email',
33-
statusCode: 400
34+
statusCode: 400,
35+
message: error.message || 'Failed to subscribe. Please try again.'
3436
})
3537
}
3638

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,53 @@
1+
import { Resend } from 'resend'
12
import { z } from 'zod'
23
import { withQuery, withTrailingSlash } from 'ufo'
34

45
export default eventHandler(async (event) => {
5-
// Get the email from body and validate its format
66
const { email } = await readValidatedBody(event, z.object({
77
email: z.string().email().trim()
88
}).parse)
99

10-
const listId = useRuntimeConfig(event).sendgrid.listId
11-
if (!listId) {
10+
const { apiKey, audienceId } = useRuntimeConfig(event).resend
11+
if (!apiKey || !audienceId) {
1212
throw createError({
1313
statusCode: 500,
14-
message: 'Missing NUXT_SENDGRID_LIST_ID env variable'
14+
message: 'Missing Resend configuration'
1515
})
1616
}
1717

18-
// Check if already in contact list
19-
await sendgrid.searchContact(event, email)
20-
.catch((err) => {
21-
if (err.statusCode !== 404) {
22-
throw createError({
23-
message: err?.data?.errors?.[0]?.message || 'Sorry, we could not verify our contact list.',
24-
statusCode: 400
25-
})
26-
}
27-
}).then((res = {}) => {
28-
if (res?.[email]?.contact?.list_ids?.includes(listId)) {
29-
throw createError({
30-
message: 'You are already subscribed to the newsletter ❤️',
31-
statusCode: 400
32-
})
33-
}
34-
})
35-
// Add to global contacts first
36-
await sendgrid.addContact(event, email)
37-
.catch((err) => {
38-
throw createError({
39-
message: err?.data?.errors?.[0]?.message || 'The email is invalid.',
40-
statusCode: 400
41-
})
18+
const resend = new Resend(apiKey)
19+
20+
// Check if the user is already subscribed
21+
const { data: contact } = await resend.contacts.get({ audienceId, id: email })
22+
if (contact && !contact.unsubscribed) {
23+
throw createError({
24+
message: 'You are already subscribed to the newsletter ❤️',
25+
statusCode: 400
4226
})
27+
}
4328

44-
// Send email to confirm registration
4529
const confirmation = generateConfirmation(event, email)
46-
const confirmationURL = withQuery(withTrailingSlash(getHeader(event, 'origin') || 'https://nuxt.com'), { email, confirmation })
30+
const origin = import.meta.dev ? getRequestURL(event).origin : 'https://nuxt.com'
31+
const confirmationURL = withQuery(withTrailingSlash(origin), { email, confirmation })
4732

48-
await sendgrid.sendEmail(event, {
49-
personalizations: [
50-
{
51-
to: [{ email }]
33+
const { error } = await resend.emails.send({
34+
from: 'Nuxt Team <team@newsletter.nuxt.com>',
35+
to: email,
36+
subject: 'Confirm your email address',
37+
template: {
38+
id: 'confirm-newsletter',
39+
variables: {
40+
confirmationURL
5241
}
53-
],
54-
from: {
55-
name: 'Nuxt Team',
56-
email: 'team@nuxt.com'
57-
},
58-
subject: 'Confirm your subscription to the Nuxt newsletter',
59-
content: [
60-
{
61-
type: 'text/html',
62-
value: `Hello,<br><br>Thank you for subscribing to the Nuxt newsletter.<br>Please complete and confirm your subscription by <a href="${confirmationURL}">clicking here</a>.<br><br>Have a wonderful day.<br>The <a href="https://nuxt.com">Nuxt</a> team.`
63-
}
64-
]
42+
}
6543
})
6644

45+
if (error) {
46+
throw createError({
47+
statusCode: 400,
48+
message: error.message || 'Failed to send confirmation email. Please try again.'
49+
})
50+
}
51+
6752
return { ok: true }
6853
})

server/types/sendgrid.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)