Ever wondered how are newsletter systems made for product websites out there? Let's create our own newsletter system where people can come your website and subscribe to your newsletter system. Since we don't want a backend always running in the background it would an amazing idea to implement it with a functions. So let's use the one of the best Cloud Functions services out there, YES Appwrite Function. Introduction
I've already drafted an article where I've implemented Appwrite functions with TS.
Let's Start
Let's first create an UI for our website, this can be anything you want to have. I'm just make this so that I can hit on appwrite functions. You can directly skip if you've your UI already implemented. Step 2: Remove all the unnecessary style.css index.html
UI Part
Step 1: I'll be using vitejs vanilla app to get a webpage quickly. You can find this on their website to scaffold an application
yarn create vite my-vue-app --template vanilla
css
and js
code. I remove counter.js
and this is how my other files look like
main.jsimport './style.css';
document.querySelector('#app').innerHTML = `
<div>
<section>
<div class="newsletter-box">
<h2>Scratch Newsletter</h2>
<form>
<input type="text" placeholder="Email" />
<button>Subscribe</button>
</form>
</div>
</section>
</div>
`
html {
background-color: blanchedalmond;
font-family: 'Courier New', Courier, monospace;
}
.newsletter-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #f4f4f4;
padding: 20px;
margin: 20px 0;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.newsletter-box h2 {
margin-bottom: 20px;
}
.newsletter-box button {
background-color: #ff6600;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.newsletter-box button:hover {
background-color: #ff5500;
}
.newsletter-box input {
width: 50%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scratch Newsletter</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
Getting started with Appwrite
Database
- Create a collection in your database
- Now let's add attributes, we just need the email in our case
- Create an API key for your function, this is needed because your function is a basically a backend application which needs access to your database and other appwrite features like accounts, storage and more.
a. Now go to your overview tab and click on Create API key
b. Add permissions just for documents.write
. Always keep your permissions to the minimal. This would help you to keep your Appwrite function more secure and also give an idea what features you're actually going to use.
c. Copy the API key we're going to use it further to add it in our function settings
Creating functions
- There are many ways to create a function in appwrite but I like to use the CLI. So you can do the installation by following the installation on this link
- Then go into your project. And scaffold the project with the below command
cli appwrite init project
cli appwrite init function ## I've used nodejs 18 for my settings
- Once you've installed the project upload it to your appwrite console
cli appwrite deploy function
Amazing we're half way through, now we will move on to actually add code to your appwrite function.
App password
So basically now that we're using Gmail as our client to send emails, we've to use it's app passwords feature to get a passwords. It's pretty intuitive, you just go to your 2factor auth -> turn it on and scroll below to add app passwords. For more information follow this article
Appwrite Functions Code
Code explaining the main driver function code please comments explaining what is happening in function, I've also added jsdocs for this function so that you can have a level of typing when you're developing the app.
Why is this important?
Well when you're prototyping your function you would want to see in your editor the obvious mistakes so that you won't have to upload multiple times to check for dumb errors which can be sorted in the editor.
Basically here the Code explanation
sendEmail()
function helps us creating a connection to our GMAIL SMTP server which we would further use to send emails. We're using app password here that we generated in the above step and would keep that in our .env
our further steps. And then our driver code is taking the email from our post call which in-turn first adds the email to our collection that we created in the above steps and then would call the sendEmail()
. The call would be triggered from our client app but you can use the Appwrite console to test this out too!
functions/Scratch Newsletter/src/main.js
import { Client, Databases, ID } from 'node-appwrite';
import nodemailer from 'nodemailer';
/**
* @param {string} toEmail
* @returns
*/
async function sendEmail(toEmail, log) {
// Create a new transporter
var transporter = nodemailer.createTransport({
host: "smtp.gmail.com",
port: 587,
secure: false,
auth: {
user: 'mnimitsavant@gmail.com',
pass: process.env.GMAIL_APP_PASSWORD,
}
});
// Send the email object
var message = {
from: "mnimitsavant@gmail.com",
to: toEmail,
subject: "test email",
html: "<h1>Welcome to Scratch Newsletter</h1><br /><p>Thank you for signing up with us!</p>",
};
// Send the email
const info = await transporter.sendMail(message);
// Log the message id
log(`Message sent ${info.messageId}`);
return info.messageId;
}
/**
* @typedef {object} Req
* @property {string} method
* @property {JSON} body
* @property {String} body.email
* @property {object} query
*/
/**
* @typedef {object} Res
* @property {Function} json
* @property {Function} send
* @property {Function} empty
*/
/**
* @param {object} context
* @param {Req} context.req
* @param {Res} context.res
* @param {object} context.log
* @param {object} context.error
*
*/
export default async ({ req, res, log, error }) => {
// Why not try the Appwrite SDK?
//
var headers = {};
headers['Content-Type'] = 'application/json';
headers['Access-Control-Allow-Origin'] = '*';
headers["Access-Control-Allow-Headers"] = "Content-Type";
headers['RAccess-Control-Allow-Methods'] = 'RANDOM';
log('Scratch Newsletter function invoked')
try {
// Create connection with Appwrite Client
// This was important to add because the websites will have cors, so first they'll send a small request to check if the server is accepting the request and then the post call will happen
if(req.method === 'OPTIONS') {
log('OPTIONS METHOD')
return res.send('', 204, headers);
} else if(req.method === 'POST') {
log('Request method is POST')
const client = new Client()
.setEndpoint('https://cloud.appwrite.io/v1')
.setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID)
.setKey(process.env.APPWRITE_API_KEY);
// Get the request body and parse
const body = req.body;
if(!body.email) {
// Send a response
log('Email is required')
return res.send('Email is required', 400, headers);
}
// Create a new database instance
const database = new Databases(client);
// Create a new document in the database
const newData = await database.createDocument(process.env.APPWRITE_DATABASE_ID, process.env.APPWRITE_COLLECTION_ID, ID.unique(), {
email: body.email
});
// Log the new data
log(newData);
if(newData.$id) {
// Send new email
await sendEmail(newData.email, log);
// Send a response
return res.send('Subscribed to the newsletter', 201, headers);
} else {
// Send a response
return res.send('Failed to subscribe to the newsletter', 409, headers);
}
} else {
// Send a response when not using POST method
return res.send('Invalid request method. Please use POST method to subscribe to the newsletter.', 405, headers);
}
} catch (err) {
// Log the error
error(err.toString());
return res.send('Some error occurred. Please try again later.');
}
};
Add secrets to our Appwrite console
Go to out function settings and then go to environment variable section and click on editor and add your respective .env
.
Edit our Client App to hit our function
Step 1.We add a function setupSubscribeButton
which helps to hit our function.
client app main.js
import './style.css';
const setupSubscribeButton = (element) => {
console.log('Setting up subscribe button');
element.addEventListener('submit', async(event) => {
event.preventDefault();
const email = event.target.elements.email.value;
if(email === '' || email === null) {
alert('Email is required');
return;
}
console.log('Subscribing', email);
const res = await fetch('https://65f335d0163e397c95de.appwrite.global', {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
email: email
}),
});
console.log('Subscribed', res.status);
});
}
document.querySelector('#app').innerHTML = `
<div>
<section>
<div class="newsletter-box">
<h2>Scratch Newsletter</h2>
<form action="" id="subscribe">
<input type="text" id="email" name="email" placeholder="Email" />
<button type="submit" onClick()=>Subscribe</button>
</form>
</div>
</section>
</div>
`
setupSubscribeButton(document.querySelector('#subscribe'));
Congratulations
nimit2801 / scratch-newsletter
Scract Newsletter
Scratch Newsletter
A newsletter implementation from scratch using Appwrite Function with Js
Tech stack used
- Appwrite
- Node.js
Architecture
- Every time someone adds their email to the newsletter on the static page. The form sends an invocation to the Appwrite function.
- This appwrite function then adds the email to the collection in the appwrite database and sends them a welcome email.
Author
Socials
Twitter: https://twitter.com/SavantNimit
LinkedIn: https://www.linkedin.com/in/nimitsavant
Twitch: https://www.twitch.tv/nimit2801
Website: https://nimitsavant.me
More with Appwrite
Top comments (1)
Amazing loved it ✨