example: adds email example

This commit is contained in:
Jessica Boezwinkle
2023-05-16 10:57:38 +01:00
parent 02b972e1ed
commit bc41f81303
19 changed files with 7646 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
MONGODB_URI=mongodb://localhost/payload-example-email
PAYLOAD_SECRET=
NODE_ENV=development
PAYLOAD_PUBLIC_SERVER_URL=http://localhost:8000

View File

@@ -0,0 +1,7 @@
module.exports = {
root: true,
extends: ['@payloadcms'],
rules: {
'@typescript-eslint/no-unused-vars': 'warn',
},
}

5
examples/email/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
build
dist
node_modules
package-lock.json
.env

63
examples/email/README.md Normal file
View File

@@ -0,0 +1,63 @@
# Payload Email Example
This example demonstrates how to integrate email functionality into Payload.
## Quick Start
To spin up this example locally, follow these steps:
1. Clone this repo
2. `cd` into this directory and run `yarn` or `npm install`
3. `cp .env.example .env` to copy the example environment variables
4. `yarn dev` or `npm run dev` to start the server and seed the database
5. `open http://localhost:8000/admin` to access the admin panel
6. Create your first user
## How it works
Payload utilizes [NodeMailer](https://nodemailer.com/about/) for email functionality. Once you add your email configuration to `payload.init()`, you send email from anywhere in your application just by calling `payload.sendEmail({})`.
1. Navigate to `src/server.ts` - this is where your email config gets passed to Payload
2. Open `src/email/transport.ts` - here we are defining the email config. You can use an env variable to switch between the mock email transport and live email service.
Now we can start sending email!
3. Go to `src/collections/Newsletter.ts` - with an `afterChange` hook, we are sending an email when a new user signs up for the newsletter
Let's not forget our authentication emails...
4. Auth-enabled collections have built-in options to verify the user and reset the user password. Open `src/collections/Users.ts` and see how we customize these emails.
Speaking of customization...
5. Take a look at `src/email/generateEmailHTML` and how it compiles a custom template when sending email. You change this to any HTML template of your choosing.
That's all you need, now you can go ahead and test out this repo by creating a new `user` or `newsletter-signup` and see the email integration in action.
## Development
To spin up this example locally, follow the [Quick Start](#quick-start).
## Production
To run Payload in production, you need to build and serve the Admin panel. To do so, follow these steps:
1. First invoke the `payload build` script by running `yarn build` or `npm run build` in your project root. This creates a `./build` directory with a production-ready admin bundle.
1. Then run `yarn serve` or `npm run serve` to run Node in production and serve Payload from the `./build` directory.
### Deployment
The easiest way to deploy your project is to use [Payload Cloud](https://payloadcms.com/new/import), a one-click hosting solution to deploy production-ready instances of your Payload apps directly from your GitHub repo. You can also deploy your app manually, check out the [deployment documentation](https://payloadcms.com/docs/production/deployment) for full details.
## Resources
For more information on integrating email, check out these resources:
<!-- Update with live blog post URL when published -->
- [Blog Post - Email 101](https://payloadcms.com/blog)
- [Email Documentation](https://payloadcms.com/docs/email/overview#email-functionality)
## Questions
If you have any issues or questions, reach out to us on [Discord](https://discord.com/invite/r6sCXqVk3v) or start a [GitHub discussion](https://github.com/payloadcms/payload/discussions).

View File

@@ -0,0 +1,4 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts"
}

View File

@@ -0,0 +1,35 @@
{
"name": "payload-example-email",
"description": "Payload Email integration example.",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_SEED=true PAYLOAD_DROP_DATABASE=true PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn copyfiles && yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
"generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:graphQLSchema",
"lint": "eslint src",
"lint:fix": "eslint --fix --ext .ts,.tsx src"
},
"dependencies": {
"dotenv": "^8.2.0",
"express": "^4.17.1",
"payload": "1.6.29",
"handlebars": "^4.7.7",
"inline-css": "^4.0.2"
},
"devDependencies": {
"@types/express": "^4.17.9",
"copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.19.0",
"nodemon": "^2.0.6",
"ts-node": "^9.1.1",
"typescript": "^4.8.4"
}
}

View File

@@ -0,0 +1,39 @@
import type { CollectionConfig } from 'payload/types'
import generateEmailHTML from '../email/generateEmailHTML'
const Newsletter: CollectionConfig = {
slug: 'newsletter-signups',
admin: {
defaultColumns: ['name', 'email'],
},
hooks: {
afterChange: [
async ({ doc, operation, req }) => {
if (operation === 'create') {
req.payload.sendEmail({
to: doc.email,
from: 'sender@example.com',
subject: 'Thanks for signing up!',
html: await generateEmailHTML({
headline: 'Welcome to the newsletter!',
content: `<p>${doc.name ? `Hi ${doc.name}!` : 'Hi!'} We'll be in touch soon...</p>`,
}),
})
}
},
],
},
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'email',
type: 'text',
required: true,
}
],
}
export default Newsletter

View File

@@ -0,0 +1,29 @@
import type { CollectionConfig } from 'payload/types'
import generateForgotPasswordEmail from '../email/generateForgotPasswordEmail'
import generateVerificationEmail from '../email/generateVerificationEmail'
const Users: CollectionConfig = {
slug: 'users',
auth: {
verify: {
generateEmailSubject: () => 'Verify your email',
generateEmailHTML: generateVerificationEmail,
},
forgotPassword: {
generateEmailSubject: () => 'Reset your password',
generateEmailHTML: generateForgotPasswordEmail,
},
},
admin: {
useAsTitle: 'email',
},
fields: [
{
name: 'name',
type: 'text',
},
],
}
export default Users

View File

@@ -0,0 +1,24 @@
import fs from 'fs'
import Handlebars from 'handlebars'
import inlineCSS from 'inline-css'
import path from 'path'
const template = fs.readFileSync
? fs.readFileSync(path.join(__dirname, './template.html'), 'utf8')
: ''
// Compile the template
const getHTML = Handlebars.compile(template)
const generateEmailHTML = async (data): Promise<string> => {
const preInlinedCSS = getHTML(data)
const html = await inlineCSS(preInlinedCSS, {
url: ' ',
removeStyleTags: false,
})
return html
}
export default generateEmailHTML

View File

@@ -0,0 +1,14 @@
import generateEmailHTML from './generateEmailHTML'
const generateForgotPasswordEmail = async ({ token }): Promise<string> =>
generateEmailHTML({
headline: 'Locked out?',
content:
'<p>Let&apos;s get you back in.</p>',
cta: {
buttonLabel: 'Reset your password',
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/reset-password?token=${token}`,
},
})
export default generateForgotPasswordEmail

View File

@@ -0,0 +1,16 @@
import generateEmailHTML from './generateEmailHTML'
const generateVerificationEmail = async (args): Promise<string> => {
const { user, token } = args
return generateEmailHTML({
headline: 'Verify your account',
content: `<p>Hi${user.name ? ' ' + user.name : ''}! Validate your account by clicking the button below.</p>`,
cta: {
buttonLabel: 'Verify',
url: `${process.env.PAYLOAD_PUBLIC_SERVER_URL}/verify?token=${token}&email=${user.email}`,
},
})
}
export default generateVerificationEmail

View File

@@ -0,0 +1,317 @@
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body,
html {
margin: 0;
padding: 0;
}
body,
html,
.bg {
height: 100%;
}
body,
h1,
h2,
h3,
h4,
p,
em,
strong {
font-family: sans-serif;
}
body {
font-size: 15px;
color: #333333;
}
a {
color: #333333;
outline: 0;
text-decoration: underline;
}
a img {
border: 0;
outline: 0;
}
img {
max-width: 100%;
height: auto;
vertical-align: top;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 900;
line-height: 1.25;
}
h1 {
font-size: 40px;
color: #333333;
margin: 0 0 25px 0;
}
h2 {
color: #333333;
margin: 0 0 25px 0;
font-size: 30px;
line-height: 30px;
}
h3 {
font-size: 25px;
color: #333333;
margin: 0 0 25px 0;
}
h4 {
font-size: 20px;
color: #333333;
margin: 0 0 15px 0;
line-height: 30px;
}
h5 {
color: #333333;
font-size: 17px;
font-weight: 900;
margin: 0 0 15px;
}
table {
border-collapse: collapse;
}
p,
td {
font-size: 14px;
line-height: 25px;
color: #333333;
}
p {
margin: 0 0 25px;
}
ul {
padding-left: 15px;
margin-left: 15px;
font-size: 14px;
line-height: 25px;
margin-bottom: 25px;
}
li {
font-size: 14px;
line-height: 25px;
color: #333333;
}
table.hr td {
font-size: 0;
line-height: 2px;
}
.white {
color: white;
}
/********************************
MAIN
********************************/
.main {
background: white;
}
/********************************
MAX WIDTHS
********************************/
.max-width {
max-width: 800px;
width: 94%;
margin: 0 3%;
}
/********************************
REUSABLES
********************************/
.padding {
padding: 60px;
}
.center {
text-align: center;
}
.no-border {
border: 0;
outline: none;
text-decoration: none;
}
.no-margin {
margin: 0;
}
.spacer {
line-height: 45px;
height: 45px;
}
/********************************
PANELS
********************************/
.panel {
width: 100%;
}
@media screen and (max-width : 800px) {
h1 {
font-size: 24px !important;
margin: 0 0 20px 0 !important;
}
h2 {
font-size: 20px !important;
margin: 0 0 20px 0 !important;
}
h3 {
font-size: 20px !important;
margin: 0 0 20px 0 !important;
}
h4 {
font-size: 18px !important;
margin: 0 0 15px 0 !important;
}
h5 {
font-size: 15px !important;
margin: 0 0 10px !important;
}
.max-width {
width: 90% !important;
margin: 0 5% !important;
}
td.padding {
padding: 30px !important;
}
td.padding-vert {
padding-top: 20px !important;
padding-bottom: 20px !important;
}
td.padding-horiz {
padding-left: 20px !important;
padding-right: 20px !important;
}
.spacer {
line-height: 20px !important;
height: 20px !important;
}
}
</style>
</head>
<body>
<div style="background-color:#F3F3F3; height: 100%;">
<table height="100%" width="100%" cellpadding="0" cellspacing="0" border="0" bgcolor="#f3f3f3"
style="background-color: #f3f3f3;">
<tr>
<td valign="top" align="left">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td align="center" valign="top">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td align="center">
<table class="max-width" cellpadding="0" cellspacing="0" border="0" width="100%"
style="width: 100%;">
<tbody>
<tr>
<td class="spacer">&nbsp;</td>
</tr>
<tr>
<td class="padding main">
<table cellpadding="0" cellspacing="0" border="0" width="100%">
<tbody>
<tr>
<td>
<!-- LOGO -->
<a href="https://payloadcms.com/" target="_blank">
<img src="https://payloadcms.com/images/logo-dark.png" width="150"
height="auto" />
</a>
</td>
</tr>
<tr>
<td class="spacer">&nbsp;</td>
</tr>
<tr>
<td>
<!-- HEADLINE -->
<h1 style="margin: 0 0 30px;">{{headline}}</h1>
</td>
</tr>
<tr>
<td>
<!-- CONTENT -->
{{{content}}}
<!-- CTA -->
{{#if cta}}
<div>
<a href="{{cta.url}}"
style="background-color:#222222;border-radius:4px;color:#ffffff;display:inline-block;font-family:sans-serif;font-size:13px;font-weight:bold;line-height:60px;text-align:center;text-decoration:none;width:200px;-webkit-text-size-adjust:none;">
{{cta.buttonLabel}}
</a>
</div>
{{/if}}
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
let email
if (process.env.NODE_ENV === 'production') {
email = {
fromName: 'Payload',
fromAddress: 'info@payloadcms.com',
transportOptions: {
// Configure a custom transport here
},
}
} else {
email = {
fromName: 'Ethereal Email',
fromAddress: 'example@ethereal.com',
logMockCredentials: true,
}
}
export default email

View File

@@ -0,0 +1 @@
export default {};

View File

@@ -0,0 +1,35 @@
/* tslint:disable */
/**
* This file was automatically generated by Payload CMS.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
collections: {
'newsletter-signups': NewsletterSignup;
users: User;
};
globals: {};
}
export interface NewsletterSignup {
id: string;
name?: string;
email: string;
createdAt: string;
updatedAt: string;
}
export interface User {
id: string;
name?: string;
email?: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
_verified?: boolean;
_verificationToken?: string;
loginAttempts?: number;
lockUntil?: string;
createdAt: string;
updatedAt: string;
password?: string;
}

View File

@@ -0,0 +1,46 @@
import dotenv from 'dotenv'
import path from 'path'
import { buildConfig } from 'payload/config'
import Users from './collections/Users'
import Newsletter from './collections/Newsletter'
dotenv.config({
path: path.resolve(__dirname, '../.env'),
})
const mockModulePath = path.resolve(__dirname, './emptyModule.js')
export default buildConfig({
admin: {
webpack: config => ({
...config,
resolve: {
...config?.resolve,
alias: [
'fs',
'handlebars',
'inline-css',
path.resolve(__dirname, './email/transport'),
path.resolve(__dirname, './email/generateEmailHTML'),
path.resolve(__dirname, './email/generateForgotPasswordEmail'),
path.resolve(__dirname, './email/generateVerificationEmail'),
].reduce(
(aliases, importPath) => ({
...aliases,
[importPath]: mockModulePath,
}),
config.resolve.alias,
),
},
}),
},
collections: [
Newsletter,
Users,
],
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
},
})

View File

@@ -0,0 +1,30 @@
import express from 'express'
import path from 'path'
import payload from 'payload'
import email from './email/transport'
require('dotenv').config({
path: path.resolve(__dirname, '../.env'),
})
const app = express()
app.get('/', (_, res) => {
res.redirect('/admin')
})
const start = async (): Promise<void> => {
await payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
email,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
},
})
app.listen(8000)
}
start()

View File

@@ -0,0 +1,38 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
"sourceMap": true,
"resolveJsonModule": true,
"paths": {
"payload/generated-types": [
"./src/payload-types.ts"
],
"node_modules/*": [
"./node_modules/*"
]
},
},
"include": [
"src"
],
"exclude": [
"node_modules",
"dist",
"build",
],
"ts-node": {
"transpileOnly": true
}
}

6919
examples/email/yarn.lock Normal file

File diff suppressed because it is too large Load Diff