labnotes

Sending email with react-email

its nicer to use their interative builder but why not just the simple thing

tags
react
email
htmlq
reactemail

Install stuff we'll need:

1
2
3
  npm install react-email @react-email/components
  npm install -D tsup typescript
  npm install resend

Which installs so much stuff…

1
2
  ls -l node_modules | wc -l
  du -sh node_modules
     336
446M	node_modules

And gives us something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  {
      "type": "module",
      "main": "./dist/index.js",
      "module": "./dist/index.mjs",
      "types": "./dist/index.d.ts",
      "files": [
          "dist/**"
      ],
      "scripts": {
          "emaildev": "email dev",
          "build": "tsup --dts --external react",
          "dev": "tsup --dts --external react --watch",
          "clean": "rm -rf dist"
      },
      "dependencies": {
          "@react-email/components": "^0.0.22",
          "react-email": "^2.1.6",
          "resend": "^3.5.0"
      },
      "devDependencies": {
          "tsup": "^8.2.3",
          "typescript": "^5.5.4"
      }
  }

A quite little tsup.config.ts:

1
2
3
4
5
6
  import { defineConfig } from 'tsup'

  export default defineConfig({
      entry: ['render.tsx', 'resend.tsx'],
      target: 'es2024'
  })

Then we need to configure typescript, like so:

// tsconfig.json

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  {
  "compilerOptions": {
    "composite": false,
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "inlineSources": false,
    "isolatedModules": true,
    "moduleResolution": "node",
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "preserveWatchOutput": true,
    "skipLibCheck": true,
    "strict": true,
    "strictNullChecks": true,
    "jsx": "react-jsx",
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "target": "es6",
  },
  "include": ["."],
  "exclude": ["dist", "build", "node_modules"]
}

Make the template

The npm run emaildev command looks for things in the emails folder, and it's really handle to have the preview while you edit.

1
  mkdir -p emails

I just played around with some different options of things to do, it works pretty well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  // emails/template.jsx
  import { Html,
           Heading,
           Container,
           Button } from "@react-email/components";
  import { CodeBlock, dracula } from "@react-email/code-block";

  const code = `export default async (req, res) => {
    try {
      const html = await renderAsync(
        EmailTemplate({ firstName: 'John' })
      );
      return NextResponse.json({ html });
    } catch (error) {
      return NextResponse.json({ error });
    }
  }`;

  export default function Email() {
    return (
      <Html>
          <Container>
              <Heading as="h1">This is an email</Heading>
            <Button href="https://willschenk.com"
                    style={{ background: "#000", color: "#fff", padding: "12px 20px" }}>
                Click me
            </Button>

             <CodeBlock
                 code={code}
                 lineNumbers
                 theme={dracula}
                 language="javascript"
             />

         </Container>
      </Html>
    );
  };

Test the templates

1
  npm run emaildev

This will open up a server on port 3000 that will let you live preview the changes that you make!

Render the template

1
2
3
4
5
6
  import Email from './emails/template';
  import { render } from '@react-email/components';

  console.log( render( <Email />, {
      pretty: true,
  } ) );
1
  node dist/render.cjs | htmlq --pretty

There's not really a big reason to do this other than to see how it works. We are going to use resend below to actually trigger the sending of the message.

Send with resend

Resend is a new service that makes it easier to send emails, and they were the ones that wrote react.email so of course the fit together nicely!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  import { Resend } from 'resend';
  import Email from './emails/template';

  if( process.env.RESEND_API === undefined ) {
      console.log( "Please set RESEND_API" );
      process.exit(1);
  }

  const resend = new Resend(process.env.RESEND_API);

  (async function() {
      const results = await resend.emails.send({
          from: 'onboarding@resend.dev',
          to:   'wschenk@gmail.com',
          subject: 'Test email',
          react: <Email />,
      });

      console.log( "Email sent" )
      console.log( results );
  })();

Testing

I'm pulling the API key out of 1password:

1
2
  # environment
  RESEND_API=op://Personal/Resend API/notesPlain

And then:

1
  op run --env-file environment -- node dist/resend.cjs
Email sent
{ data: { id: '6185f47f-f825-46f1-8557-a432ad2ccfc1' }, error: null }

Previously

labnotes

Using 1password from a script

better than keeping keys around

tags
1password
cli

Next

howto

Adding authentation with clerk

make it simple

tags
auth
nextjs
clerk
vercel