Lag en serverless webhook som sender epost når nytt innhold er publisert

Epost-ikon på en mobilskjerm.

Med Gatsby Functions er det enkelt å lage en serverless-funksjon som automatisk sender epost til mottakere som ønsker det. Funksjonen kan trigges av for eksempel at nytt innhold publiseres i et CMS.

I en serie med artikler har jeg vist hvordan jeg lagde nye nettsider til sameiet der jeg bor ved hjelp av React-rammeverket Gatsby, Chakra UI og med brukerautentisering via Auth0. Les del 1 her: Slik bygget jeg nye nettsider til sameiet med Gatsby og Chakra UI

Noe av poenget med nettsidene var å ha et sted å publisere nyttig eller viktig informasjon til beboerne i sameiet. Jeg innså imidlertid ganske fort at det var tungvint å manuelt sende epost for å gi beskjed til beboerne hver gang vi hadde publisert ny informasjon i vårt headless CMS-system Contentful.

Med Gatsby Functions er det forholdsvis enkelt å lage et API som sender epost til "abonnenter". Jeg skal i denne artikkelen gå gjennom hvordan jeg satte opp dette, slik at Contentful sender en POST-request til mitt API. Så sjekker API-et først at den som gjør POST-requesten har lov til å gjøre det, og deretter henter det en liste over alle sameiets registrerte brukere og sender epost til de som har valgt å motta varsling om nytt innhold.

For å sende epost valgte jeg å bruke tjenesten Sendgrid. Jeg vurderte også Nodemailer, men Sendgrid har noen fine muligheter til å sette opp epost-maler, slik at jeg kan hente ut URL, tittel og ingress til artikkelen som er publisert i Contentful, og så få Sendgrid til å fylle inn dette i epost-malen før epostene sendes ut.

For å sette opp alt dette, var det tre ting jeg måtte gjøre:

  • Opprette en webhook i Contentful
  • Lage en serverless-funksjon med Gatsby Functions
  • Lage en epost-mal i Sendgrid

Selv om det jeg har laget er for et ganske spesifikt formål, kan det enkelt tilpasses andre behov. Og bruker du ikke Gatsby, er det ikke store endringene som skal til for å få det til å fungere med for eksempel Netlify Functions eller Next.js API routes. Eventuelt noe annet — koden er temmelig lik slik du ville gjort det i Node.js med Express-rammeverket.

Sette opp en webhook i Contentful

Alt innholdet på nettsidene til sameiet publiseres i headless CMS-løsningen Contentful. Under Settings-menyen i Contentful har du et valg som heter Webhooks. Her opprettet jeg en webhook som skal trigges kun når nytt innhold blir publisert, og da sende en POST-request til epost-API-et jeg har tenkt å lage.

Jeg trykker Add Webhook og gir den et navn, for deretter å oppgi at det skal være en POST-request som skal gå til URL-en for epost-API-et mitt.

Skjermbilde, oppsett av webhook i Contentful.

Under Content Events oppgir jeg at webhooken skal kalles kun når nytt innhold publiseres.

Skjermbilde, sette opp triggers for webhook i Contentful.

For at ikke hvem som helst skal kunne kalle epost-API-et mitt og spamme alle brukerne, velger jeg å opprette en Secret header som er et key/value-par som sendes i headeren når API-kallet gjøres. Altså en hemmelig nøkkel som API-et vil sjekke før det tillater at epost blir sendt.

Jeg kalte min hemmelige nøkkel for CONTENTFUL_WEBHOOK_SECRET og brukte en passordgenerator til å generere et langt og tilfeldig passord bestående av tall, bokstaver og spesialtegn.

Skjermbilde, sette opp secret header til webhooken

Lage epost-API-et med Gatsby Functions

Gatsby Functions er en Express-lignende måte å bygge API-er til Gatsby-prosjektene dine. Det at det er snakk om serverless-funksjoner betyr selvfølgelig ikke at det ikke er servere, men rett og slett at du slipper å tenke på serverne — alt du trenger å fokusere på er å skrive koden. Sånt liker i hvert fall jeg.

For å lage en ny Gatsby-funksjon oppretter du bare en JavaScript- eller TypeScript-fil i mappen src/api/ og eksporterer en handler-funksjon som tar to parametere — req (request) og res (response).

Jeg opprettet en fil som het email-alert-users.ts i mappen api/admin-users/ (bruk .js hvis du ikke bruker TypeScript). Det betyr at API-endepunktet mitt blir https://url-til-nettstedet-mitt/admin-users/email-alert-users.

I første del av koden sjekker jeg først at HTTP-metoden faktisk er av typen POST. Deretter sjekker jeg at headeren inneholder CONTENTFUL_WEBHOOK_SECRET og at den samsvarer med den hemmelige nøkkelen jeg har lagret som en miljøvariabel (lokalt i en .env-fil, og hos Netlify under Deploy Settings og Environment. Pass på at du ikke lagrer denne nøkkelen noe sted som havner på Github — sjekk at du har .env* i .gitignore-filen din.

Her er første del av koden, med de nødvendige sjekkene:

1// src/api/admin-users/email-alert-users.ts
2
3import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby';
4const ManagementClient = require('auth0').ManagementClient;
5const sgMail = require('@sendgrid/mail');
6
7export default async function handler(
8  req: GatsbyFunctionRequest,
9  res: GatsbyFunctionResponse
10) {
11  if (req.method !== `POST`) {
12    return res.status(405).json({
13      error: 'method not allowed',
14      error_description: 'You should do a POST request to access this',
15    });
16  }
17
18  // Check if secret key received from Contentful web hook matches the one in the .env file
19  if (
20    req.headers.contentful_webhook_secret !==
21    process.env.CONTENTFUL_WEBHOOK_SECRET
22  ) {
23    return res.status(401).json({
24      error: 'unauthorized',
25      error_description: 'The Contentful web hook secret key is not correct',
26    });
27  }
28
29// ...code continues

Bruker du ikke TypeScript, tar du vekk GatsbyFunctionRequest og GatsbyFunctionResponse, dette er bare for typesjekking.

ManagementClient som settes opp nesten øverst, er for å kunne koble oss til Auth0 sitt Management API for at jeg skal få hentet ut liste over alle brukerne som skal ha epost. Denne brukerlisten kan du selvfølgelig hente fra andre steder også, for eksempel en database.

Sette opp epost-innholdet

Jeg ønsker at epostene som sendes til brukerne skal inneholde følgende, i tillegg til en standardtekst om at det er nytt innhold (dette setter jeg opp hos Sendgrid):

  • Tittel og ingress
  • URL til artikkelen

På sameiets nettsider er deler av innholdet kun tilgjengelig for innloggede brukere, og dette innholdet ligger på såkalte *client-only routes* i Gatsby-prosjektet mitt, nærmere bestemt under ruten /informasjon. Alle åpne artikler ligger under /blog. Det betyr at jeg først må sjekke om det er en privat artikkel eller ikke, og så bygge opp riktig URL ut fra det. Når webhooken hos Contentful gjør en POST-request til API-et mitt, sender den med masse info om hva som er publisert i body. Jeg har laget et felt i Contentful som heter privatePost som kan være true eller false (dette er en sjekkboks man huker av når man legger inn en artikkel i Contentful-editoren). Dermed er det lett å lage riktig URL ved å la API-et sjekke dette feltet først.

Jeg henter også ut artikkel-tittel og ingress (kort intro om artikkelen) på denne måten:

1// src/api/admin-users/email-alert-users.ts
2
3  let articleURL;
4  if (req.body.fields.privatePost.nb === true) {
5    articleURL = `https://gartnerihagen-askim.no/informasjon/post/${req.body.fields.slug.nb}/${req.body.sys.id}`;
6  } else {
7    articleURL = `https://gartnerihagen-askim.no/blog/${req.body.fields.slug}/${req.body.sys.id}`;
8  }
9
10  const articleTitle = req.body.fields.title.nb;
11  const articleExcerpt = req.body.fields.excerpt.nb;
12
13// ...code continues

Hente alle brukere som ønsker epost

Nå som selve epost-innholdet er hentet ut, kobler jeg meg til Auth0 sitt Management API og ber om tillatelsen ("scope") read:users. Så bruker jeg metoden getUsers() til å hente ut alle brukerne. Deretter bruker jeg en kombinasjon av .filter og .map til å lage en liste over kun brukere som har abonnert på epost. Info om dette har jeg fra før av lagret i user_metadata-feltet for hver bruker i Auth0.

Jeg har nå en liste over alle brukere som vil ha epost. Deretter må jeg sende over en API-nøkkel for å få tilgang til min Sendgrid-konto med sgMail.setApiKey(process.env.SENDGRID_API_KEY), og så sette opp epost-innholdet som jeg lagrer i konstanten msg. Dette er en array med epostmottakere, avsenders epostadresse, samt en templateId. Sistnevnte er ID-en til en epost-mal jeg har satt opp hos Sendgrid. Under dynamic_template_data kan jeg sende med informasjon om artikkel-URL, tittel og ingress til artikkelen, som så vil fanges opp av Sendgrid og fylles inn i epost-malen (jeg går straks igjennom hvordan du lager denne malen hos Sendgrid).

Til slutt sendes eposten med sgMail.sendMultiple(msg).

1// src/api/admin-users/email-alert-users.ts
2
3// Connect to the Auth0 management API
4  const auth0 = new ManagementClient({
5    domain: `${process.env.GATSBY_AUTH0_DOMAIN}`,
6    clientId: `${process.env.AUTH0_BACKEND_CLIENT_ID}`,
7    clientSecret: `${process.env.AUTH0_BACKEND_CLIENT_SECRET}`,
8    scope: 'read:users',
9  });
10
11  try {
12    const users = await auth0.getUsers();
13
14    // Filter out only those users that have subscribed to email alerts
15    // This is defined in the user_metadata field on Auth0
16    const userEmails = users
17      .filter((user) => {
18        return (
19          user &&
20          user.user_metadata &&
21          user.user_metadata.subscribeToEmails === true
22        );
23      })
24      .map((user) => user.email);
25
26    // using Twilio SendGrid's v3 Node.js Library
27    // https://github.com/sendgrid/sendgrid-nodejs
28    sgMail.setApiKey(process.env.SENDGRID_API_KEY);
29
30    const msg = {
31      to: userEmails,
32      from: 'Boligsameiet Gartnerihagen <post@gartnerihagen-askim.no>',
33      templateId: 'd-123456789',  // The ID of the dynamic Sendgrid template
34      dynamic_template_data: {
35        articleURL,
36        articleTitle,
37        articleExcerpt,
38      },
39    };
40
41    await sgMail.sendMultiple(msg);
42
43// ...code continues

Hvis alt har fungert som det skal, returnerer jeg status 200 og litt info. Siden alt dette gjøres inni en try/catch-blokk, fanges eventuelle feil opp, og jeg returnerer isåfall en feilkode og litt info om feilen.

1res.status(200).json({
2      body: {
3        status_code: 200,
4        status_description: 'Emails are sent to all subscribed users',
5        userEmails,
6      },
7    });
8  } catch (error) {
9    res.status(error.statusCode).json({
10      error: error.name,
11      message: error.message,
12      status_code: error.statusCode || 500,
13      error_description: error.message,
14    });
15  }
16}

Hvordan Sendgrid-malen er satt opp

Jeg kunne selvfølgelig "hardkodet" eposten som skal sendes ut, men med "dynamic Templates" i Sendgrid får du mulighet til å fylle ut eposten med skreddersydd informasjon. I mitt tilfelle informasjon om artikkelen som er publisert, inkludert en URL.

For å sette opp en dynamisk epostmal i Sendgrid, går du til sendgrid.com, logger deg inn og velger Email APIDynamic Templates fra innstillingsmenyen. Trykk deretter Create a Dynamic Template, deretter Add Version. Du kan nå velge å bruk Sendgrids design-editor for å sette opp utseendet på eposten din, eller du kan bruke en HTML-editor. Siden jeg er nerd, brukte jeg HTML-editoren og la inn litt standardtekst.

De stedene i teksten der du vil sette inn dynamisk innhold — som tittel, URL, osv. — der kan du sette inn variabelnavn. I epost-API-et mitt bruker jeg for eksempel variabelnavnet articleTitle for artikkeltittelen jeg mottar fra CMS-et mitt. For å bruke dette i Sendgrid-malen, setter du bare inn {{articleTitle}} i HTML-koden.

På høyre side (se skjermbilde under) kan du se hvordan eposten kommer til å se ut. Trykker du Test Data i menyen på toppen kan du legge inn testdata for de ulike variablene du bruker i epost-malen for å sjekke at alt ser OK ut.

Skjermbilde, oppsett av dynamiske epost-maler i Sendgrid.

Det var det! Vi har nå laget en varslingstjeneste som sender ut epost hver gang nytt innhold publiseres i vår headless CMS-løsning.

Etter at jeg lagde denne løsningen, lagde jeg også funksjonalitet i brukeradmin-grensesnittet til sameiets nettsider, slik at administratorer kan skru av og på epostvarsling for brukerne. Brukere kan også selv skru av og på epost-varsling via "Min side" på sameiets nettsider:

Skjermbilde fra Min Side på Boligsameiet Gartnerihagens nettsider. Her kan brukeren selv skru av og på epost-varsling.

Du finner kildekoden til hele prosjektet på min Github. Hvis du bare vil kikke på kildekoden til Gatsby-funksjonen for å sende epost, så finner du den her.

Les også hvordan jeg lagde brukeradmin-dashbordet i denne artikkelen.

Publisert: 10. oktober 2021 (oppdatert: 15. juli 2022)