Final Project (Node) for Web 2

Step 1 - Setting Up The Initial Project Files

Create a folder named web2-final-project. Make sure NOT to put this folder inside a folder that is already a Git repository. In the following steps you will initialize a Git repository for the folder.

Open the folder in VSCode.

Add a README.md file to the project folder.

Add a heading to the readme file that says Web 2 Final Project.

Run this command (from the project folder) to initialize it as an NPM project:

npm init -y

This will create a package.json file in the project folder.

Add a .gitignore file to the project folder and put this in it:

node_modules/

This will keep the Git repository from becoming bloated with all the NPM packages that will be installed in the node_modules folder.

Open the terminal in VS Code, cd into the project folder (if you are not already there) and initialize a Git repository:

git init

Add and commit the files in the project folder, your commit message can be Initial Commit

Publish the repository to GitHub.

Install Express

Express is a package that allows you to build web servers with JavaScript code.

Install the Express package:

npm install express

Create a file named app.js in the project folder and put this code in it:

const port = 8080; // We'll run the server on port 8080

// IMPORTS
const express = require('express');
const app = express();

// MIDDLEWARE


// ROUTES
app.get('/', (req, res) => {
   res.send('<h1>Hello World from Express!</h1>');
});

// START THE SERVER
const server = app.listen(port, () => {
   console.log("Waiting for requests on port %s", port);
});

Now run the app (server) by entering this command:

node app.js

Now open a browser tab and navigate to http://localhost:8080. You should see a page that says 'Hello World from Express!'.

To stop the server, click in the terminal and then press ctrl + c.

Add Static Pages and Files

Create a folder named public (put it in the project folder).

Inside the public folder, add a file named static-page.html and put this code inside of it:

<!DOCTYPE html> 
<html lang="en"> 
	<head> 
	  	<title>Static Page</title>
	  	<meta charset="utf-8"> 
	  	<meta name="viewport" content="width=device-width, initial-scale=1" /> 
	</head>
	<body>
		<h1>This is a static page</h1>
	</body>
</html>

Now we'll configure the server to use the 'public' folder as a static one (in NodeJS it's common to use a 'public' folder for your static files). This will be the doc root directory for your static web pages, and other files (such as .css, .js, and image files).

Add this code to app.js, put it underneath the comment that says MIDDLEWARE:

app.use(express.static('public'));

Now start the server by entering node app.js (if the server is already started, then stop it with ctrl + c), then open this URL in the browser: http://localhost:8080/static-page.html

Create these 3 folders inside the public folder:

  1. images
  2. css
  3. js

Then put an image in the images folder (any old jpg or png will do).

In the js folder, add a file named main.js and put this code in it:

console.log("This is main.js...");

In the css folder, add a file named main.css and put this code in it:

h1{ color: midnightblue; }

Now update the static-page.html to look like this (note that you'll have to use the proper name for your image file by setting the SRC attribute of the IMG element):

<!DOCTYPE html> 
<html lang="en"> 
<head> 
  <title>Static Page</title>
  <meta charset="utf-8"> 
  <meta name="viewport" content="width=device-width, initial-scale=1" /> 
  <link rel="stylesheet" type="text/css" href="/css/main.css">
  <script src="/js/main.js"></script>
</head>
<body>
  <h1>This is a static page</h1>
  <img src="/images/YOUR-IMAGE-NAME-GOES HERE"> <!--set the path to YOUR image file-->
</body>
</html>

Note that the links to the static files start with a forward slash. These paths are known as doc root relative links.

Stop the server (ctr + c). Then restart it. Now visit the static page in your browser (http://localhost:8080/static-page.html), and verify tha the image appears, and that the links to the .css and .js files are working.

Nodemon

It get's to be a little tedious when you have stop and start the server every time you make a change. Luckily there's an NPM package that we can use so that the server will automatically restart whenever we save changes.

Run this command (from the project folder) to install the Nodemon package:

npm install nodemon --save-dev

Note that the --save-dev option will save this a 'development dependency' in the package.json file. A development dependency is a package that you only need when you are developing your project, and it will not be included in code that gets deployed to the live/production server.

Now stop the server (if it's running) and we'll start it using Nodemon by entering this command (go ahead and enter the command in the terminal):

npx nodemon app.js

Now, whenever you save changes, Nodemon will automatically restart the server and reload the page in the browser for you.

You can stop nodemon from running in the terminal by pressing ctrl + c.

At this point, you should commit your changes and push the main/master branch to GitHub:

git add .
git commit -m "step1 done"
git push origin master

Then create a branch, which will serve as a snapshot of the current state of your project, by running these commands:

git branch step1-complete
git push origin step1-complete

Step 2 - Set Up the EJS Templates

Now we'll use templates to make it easier to manage our pages. There are lots of templating packages that you can use for this, but we'll be using EJS (which stands for 'embedded javascript').

Install the EJS package:

npm install ejs

Create a folder named views in the project folder.

In the views folder, create a file named default-layout.ejs and put this code in it:

<!DOCTYPE html> 
<html lang="en"> 
	<head> 
		<title><%= title %></title>
	  	<meta charset="utf-8"> 
	  	<meta name="viewport" content="width=device-width, initial-scale=1" /> 
		<link rel="stylesheet" type="text/css" href="/css/main.css">
		<script src="/js/main.js"></script>
	</head>
	<body>
		<header>
			<h1>Header</h1>
		</header>
		<nav>
			<ul>
	      	<li><a href="/">Home</a></li>
	        	<li><a href="/blog">Blog</a></li>
	        	<li><a href="/contact">Contact</a></li>
	      </ul>
		</nav>
		<div id="content">
			<main>
				<%- content %>
			</main>
		</div>
		<footer>
			Footer
		</footer>
	</body>
</html>

This is a template that can be used for dynamic pages. Note that there are placeholders (variables) in the TITLE and MAIN elements. We'll inject content into these placeholders when a page is requested.

Now add this to the MIDDLEWARE section in app.js:

app.set('view engine', 'ejs');

This sets up our app to use EJS templates (as mentioned, there are many other templating packages available, so we need to tell our app that we're using EJS).

By default, the app will look for .ejs files in the views folder.

Now let's update the route to our home page to look like this:

app.get('/', (req, res) => {
   res.render('default-layout', {
      title: "Default Layout",
      content: "<h1>This is the default layout</h1>"
   });
});

Now run the app (npx nodemon app.js) and open the home page in the browser (localhost:8080). You should see the default layout template with the title and content injected from the route code. Note that the render() method is called on the 'response' object in order to render a template. The first parameter passed into render() is the name of the template to use (without the .ejs file extension). The second parameter is an object with properties that match the placeholders in the template.

Now we'll split the default-layout into a few different template files so that we can re-use the files for other pages. These files are called 'partials'.

Inside the views folder, create a folder named partials.

Create a file named top.ejs inside the partials folder, and put this code into it:

<!DOCTYPE html> 
<html lang="en"> 
	<head> 
		<title><%= title %></title>
	  	<meta charset="utf-8"> 
	  	<meta name="viewport" content="width=device-width, initial-scale=1" /> 
		<link rel="stylesheet" type="text/css" href="/css/main.css">
		<script src="/js/main.js"></script>
	</head>
	<body>
		<header>
			<h1>Header</h1>
		</header>
		<nav>
			<ul>
				<li><a href="/">Home</a></li>
				<li><a href="/blog">Blog</a></li>
				<li><a href="/contact">Contact</a></li>
	     	</ul>
		</nav>
		<div id="content">

Create a file named bottom.ejs inside the partials folder, and put this code into it:

		</div><!-- end of #content div-->
		<footer>
			Footer
		</footer>
	</body>
</html>

Now we can re-use these 'partial' templates to create templates for other pages that we create, let's do that now.

Update the default-layout.ejs to look like this:

<%- include('partials/top') %>
<main>
	<h1><%- title %></h1>
	<%- content %>
</main>
<%- include('partials/bottom') %>

Notice how this version of the default-layout template file 'includes' the top and bottom partial templates. Now we can re-use the partial files in other templates.

Run the app and go to http://localhost:8080 in the browser, it should look somewhat like it did before, but behind the scenes it's using partial templates to create the page.

Now we can re-use the partial templates in other .ejs files.

Set Up the Home Page

In the views folder, create a file named home.ejs and put this code into it:

<%- include('partials/top') %>
<main>
  <h1>Welcome to My Website</h1>
  <p>This page will soon show a list of links to blog posts that I have written (in markdown files)</p>
</main>
<%- include('partials/bottom') %>

Now, in app.js we need to update the route for the home page to look like this:

app.get('/', (req, res) => {
  res.render('home', {
     title: "My Home Page"
  });
});

Now, run the server (if it's not already running) and then navigate to localhost:8080 in the browser. You should see the 'home' template, which includes the 'top' and 'bottom' partial templates.

Set Up the Blog List Page

Create a file in the views folder named blog-list.ejs and put this code into it:

<%- include('partials/top') %>
<main>
<h1>TODO: The Blog List Page</h1>
</main>
<%- include('partials/bottom') %>

Now add this route to app.js (put it in the ROUTES section of app.js, after the route for the site home page):

app.get('/blog', (req, res)=>{
  res.render('blog-list', {
    title: "Blog List"
  });
});

Now visit this URL in the browser to confirm that it's working: http://localhost:8080/blog.

Set Up the Blog Post Page

Create a file in the views folder named blog-post.ejs and put this code into it:

<%- include('partials/top') %>
<main>
	<h1><%= title %></h1>
	<p><%= description %></p>
	<p><%= author %> <%= published %></p>
	<%- content %>
</main>
<%- include('partials/bottom') %>

Now add this route to app.js:

app.get("/blog/:post", (req, res) => {
   res.send("The <b>:post</b> param is set to: " +  req.params.post);
});

Note that the route includes a colon (in the first parameter that we are passing into the get() method of the app). This means that we are declaring a parameter named post.

Now start the server and navigate to these URLs and observe the response that is sent to the browser:

  1. http://localhost:8080/blog/foo
  2. http://localhost:8080/blog/bar
  3. http://localhost:8080/blog/sample-blog-page

The beautiful thing about a route that uses 'route parameters' is that we can use this single route to show all of our blog pages.

We'll be working with this parameter later in the project. But for now let's just set up the template that this route will use. Update the route to look like this:

app.get("/blog/:post", (req, res) => {
  console.log("The :post param is set to: " +  req.params.post);
  res.render("blog-post", {
    title: "Some Title",
    description: "Some Description",
    author: "Some Author",
    published: "Some Date",
    content: "Some content..."
  });
});

Notice how we are passing data into the .ejs template to set values for its placeholders. The second parameter for the render() method is an object that has properties (keys) that match the names of the placeholders in blog-post.ejs.

Set Up the Contact Page

Now we'll create another page template for the site's contact page. Create a file in the views folder named contact.ejs and put this code in it:

<%- include('partials/top') %>
<main>
  <h1>Contact Me</h1>
  <form method="POST" action="/contact/submit">
      <label>First Name:</label>
      <br>
      <input type="text" name="firstName">
      <br>

      <label>Last Name:</label>
      <br>
      <input type="text" name="lastName">
      <br>

      <label>Email:</label>
      <br>
      <input type="text" name="email">
      <br>  

      <label>Comments:</label>
      <br>
      <textarea name="comments"></textarea>
      <br>
      <input type="submit" value="SUBMIT">   
  </form>
</main>
<%- include('partials/bottom') %>

Notice how the action attribute is set to /contact/submit on the form element, we'll discuss that a bit more in a minute. But first, let's define a route for the contact page.

Add this route to app.js, put it to the ROUTES section:

app.get('/contact', (req, res) => {
  res.render('contact', {
     title: "Contact Me"
  });
});

Run the app, and visit this URL in your browser localhost:8080/contact. You should see the contact page.

If you submit the contact form now, you'll see an error message saying 'Cannot POST /contact'. To fix this, we need to add a route to app.js that handles POST requests to the /contact route.

Add this route to app.js (put it in the ROUTES section, just after the first /contact route code):

app.post('/contact/submit', (req, res) => {
  res.send("<h1>TODO: Handle contact form posts</h1>");
});

Note that this route invokes the post() method on the app object, which means that it will handle POST requests. When a form is submitted, the browser will send the data to it's destination as a POST request (we'll learn much more about POST requests in the Web 3 and 4 classes).

Go to http://localhost:8080/contact in the browser and submit the form. You should end up at the /contact/submit route, and see a 'TODO' message in the browser window.

In order get the data that was entered into the form, we need to install a package named body-parser. Run this command from the terminal to install it:

npm install body-parser

Now import it by adding this to the IMPORTS section in app.js:

const bodyParser = require('body-parser');

This package/module will allow the app to get the data entered into a form by adding a 'body' property to the request object.

Now add this code to the MIDDLEWARE section in app.js (it tells the app to 'use' the body parser package):

// allow the app to get data for form submits
app.use(bodyParser.urlencoded({ extended: true }));

Update the POST route for /contact/submit (in app.js) to look like this:

app.post('/contact/submit', (req, res) => {
  res.send("<h1>TODO: Handle contact form posts</h1>" + JSON.stringify(req.body));
});

Because of the bodyParser module, we can now access a 'body' property of the request object (the req parameter). We'll learn a lot more about 'requests' and 'the request body' in the Web 3 and 4 classes.

Now, if you submit the form from your browser, you should see the data you entered into the form.

We'll finish this route later by adding code that sends an email to you whenever someone submits the form.

At this point, you should commit your changes and push the main/master branch to GitHub:

git add .
git commit -m "step 2 done"
git push origin master

Then create a branch, which will serve as a snapshot of the current state of your project, by running these commands:

git branch step2-complete
git push origin step2-complete

Step 3 - Converting Markdown Files into HTML

Markdown is an excellent format for keeping notes because of the minimal syntax. For this web application, we'll write our blog posts in markdown and then convert the markdown files into HTML pages. We'll use a few NPM packages to do this.

Add a folder named blog to your project folder.

Then put a sample blog file in it, named sample-blog-page.md, and paste this markdown code into it:

---
title: "This is just a sample blog post"
author: "John Doe"
description: "A nice description of this post"
published: "2024-3-1"
---
# One hash tag for an H1 affect

## This is an H2 (two hash tags)

### This is an H3 (3 hash tags)

# Use Ctrl+Shift+V to preview a markdown file in VSCode

this is **bold** text

This is a link [this is the link text](https://www.google.com)

### Here's a JavaScript code sample:
```js
if(this){

}
```
The above code is a JavaScript sample.

## Lists
### Ordered Lists
1. Item 1
1. Item 2
1. Item 3

### Unordered Lists
- Item 1
- Item 2
- Item 3

## Here's how you can put images in markdown:
![This is an eagle](/images/eagle.png)

Note that you'll have to update last line so that it displays the image that you have in your images folder.

Also note that at the top of the file, in between the triple dashes, we can add metadata about the blog post. This is known as front matter and it's similar to the head element of an HTML file. All of your markdown files should include front matter that has a title, description, and author property. The published property can be omitted, but then (as you'll see), the file will not be published to your website. This allows you to keep blog pages off your website until they are ready to be published.

In order to convert markdown files to HTML, we'll use the exact same module that you created in the node sample project. But first, you need to install the gray-matter and markdown-it packages by running this command in the terminal (you can install both packages with a single command):

npm install gray-matter markdown-it

Now create a folder named modules inside the project folder.

Create a file named markdown-helpers.js inside the modules folder and put this code into it:

const fs = require('fs');
const matter = require('gray-matter'); // converts md file (with gray matter) into an object
const md = require("markdown-it")({html:true});// html:true allows you to put HTML tags in the markdown files

/**
 * Converts a markdown file into an object that includes properties
 * for the file's front matter, a 'content' property which consists
 * of the files' markdown code, and an 'html' content which consists
 * of the markdown code that has been converted to HTML
 * 
 * @param {string} filePath   The path to the markdown file
 * @returns {object}
 */
function convertMarkdown(filePath){
  const obj = matter.read(filePath);
  if(obj){
      obj.html = md.render(obj.content);   
  }
  return obj;
}

/**
 * Takes a path to a markdown file, reads the front-maktter (metadata) 
 * and returns an object that that includes the title, author, published
 * date of the file. 
 * @param {string} filePath   The path to the markdown file 
 * @returns {object}          An object that has a title, author, and published property
 */
function getMarkdownMetaData(filePath){
  const obj = matter.read(filePath);
  const metaData = {
      title: obj.data.title || "No Title",
      author: obj.data.author || "No Author",
      description: obj.data.description || "No Description",
      published: obj.data.published || false
  }
  return metaData;
}

/**
 * Gets a list of .md files in a given folder, extracts the metadata,
 * and creates a doc root relative path to be used as a hyper link
 * @param {string} folderPath   The path to the folder that contains markdown files 
 * @returns {array<object>}     An array of objects that has a title, author, published, and link property
 */
function getBlogList(folderPath){
  const blogFiles = fs.readdirSync(folderPath); 
  const blogInfoList = [];
  blogFiles.forEach(fileName => {
    if(fileName.endsWith(".md")){
      const blogInfo = getMarkdownMetaData(folderPath + fileName);
      if(blogInfo.published){
        blogInfo.link = "/blog/" + fileName.replace(".md","")
        blogInfoList.push(blogInfo)
      }
    }
  });
  return blogInfoList;
}

exports.convertMarkdown = convertMarkdown;
exports.getMarkdownMetaData = getMarkdownMetaData;
exports.getBlogList = getBlogList;

This is the same module that we created in our node sample project, BUT I've added a function named getBlogList(). There's a lot going on in this function. So we need to take the time to step through it with the debugger until we understand it. Please remind me to do this in class.

The getBlogList() function uses the fs module to get all the .md files within a specified folder. It then loops through the files and creates 'meta data' object for each one. It then adds each of these objects to an array, and then returns the array.

We'll use this array to generate a list of links to all the blog pages that are 'published' to our website.

Note that the function makes use of the getMarkdownMetaData() function to get a 'meta data' object for each .md file. It then checks to see if the meta data published property is truthy. If so, the object is added to the array that the function returns. If not, the object is omitted from the returned array. This mechanism allows you to have markdown files on your site that you are still working on.

Revisit the Blog List Page

We are now ready to make use of the markdown-helpers module. Add this code to the IMPORTS section of app.js:

const {getBlogList, convertMarkdown} = require("./modules/markdown-helpers")
const pathToBlogFolder = __dirname + '/blog/'; 

The first line imports two of the functions from our markdown-helpers module. The second line sets a variable that will be a path to the blog folder that has all the markdown files in it.

Update the /blog route (in app.js) to look like this:

app.get('/blog', (req, res)=>{
  const blogList = getBlogList(pathToBlogFolder);
  res.send(JSON.stringify(blogList));
});

If you run the app, and then navigate to localhost:8080/blog you should see an array that has one object in it. The object should include the meta data from our sample markdown file (in the blog folder).

Now we are ready to start working on the page that lists all of your 'published' blog posts. Update blog-list.ejs (in the views folder) to look like this:

<%- include('partials/top') %>
<main>
  <h1>Blog Posts</h1>
  <ul>
  <% for (let i = 0; i < posts.length; i++) { %>
      <li>
         <a href="<%=posts[i].link%>"><%=posts[i].title%></a>
         <p><%=posts[i].description%></p>
         <p><%=posts[i].author%></p>
         <p><%=posts[i].published%></p>
      </li>
  <% } %>
  </ul>
</main>
<%- include('partials/bottom') %>

The syntax is a little strange, but with EJS templates you can embed JavaScript code in between the angle bracket and percent characters, which is where a for loop is set up. The loop will iterate through an array of 'post' objects that we'll pass through from the /blog route in app.js. This array will be the same one that is returned by the getBlogList() function.

Now update the /blog route in app.js to look like this:

app.get('/blog', (req, res)=>{
  const blogList = getBlogList(pathToBlogFolder);
  res.render('blog-list', {
    title: "Blog",
    posts: blogList
  });
});

Run the app, and navigate to localhost:8080/blog. You should see a list that includes one hyperlink (we only have one .md file in the blog folder, but if you add more, they should show up in the list too).

The hyperlink doesn't work yet, we'll work on that route in the next step. But hover over the link and note the path that it's pointing to. You should see that it points to our sample blog page markdown file.

We could optimize the code by declaring and initializing the blogList constant before the route (rather than inside the route's callback function). Let me know if your interested in learning more about this.

Revisit the Blog Post Route

Remember the 'route parameter' that we used for the route that displays a blog page? Well, now we can make use of it. The parameter should match the name of one of the .md files in the blog folder, except that the parameter will not include the .md extension.

Update the /blog/:post route (in app.js) to look like this:

app.get("/blog/:post", (req, res) => {
  try{
    const pathToFile = pathToBlogFolder + req.params.post + ".md";
    console.log("Markdown file: " + pathToFile);
    const obj = convertMarkdown(pathToFile);
    res.render('blog-post', {
       title: obj.data.title,
       description: obj.data.description,
       author: obj.data.author,
       published: obj.data.published,
       content: obj.html
    });
  }catch(error){
    console.log(error);
    res.status(404).redirect("/404");
  }
});

Now navigate to this URL in your browser: http://localhost:8080/blog/sample-blog-page and you should see your first blog page!

Notice that we used a try/catch block in case there is no markdown file that matches the route :post parameter. This would cause our program to 'throw' an error and we can prevent it from crashing by 'catching' it. To see how this works, navigate to this URL: http://localhost:8080/blog/foo. In this case we send a status code of 404 and redirect to a 404 page that we have not yet created. We'll do that in the next step.

The core functionality of your final project is in place! When you add an .md file to the blog folder, it will automatically appear as a blog page on your website. To demonstrate this, create a file named another-sample-blog-page.md inside the blog folder, and put this code into it:

---
title: "This is another sample blog post"
author: "John Doe"
description: "A nice description of this post"
published: "2024-3-11"
---
# Another Sample Post

### Here's a JavaScript code sample:
```js
if(this){

}
```

Then refresh the blog list page in your browser (http://localhost:8080/blog/) and you should now see that their are two items in the list.

That will do it for this step! I don't expect you to fully understand all of the code that we used in this step, but I do expect you to make a goal of understanding it. Stepping through the code with the debugger over and over until things start to make sense is a good way to reach that goal. When you land your fist job in the real world, you will likely start by working on a project that has lots of complicated code. So you must get good at using the debugger to step through the code to understand how it works.

At this point, you should commit your changes and push the main/master branch to GitHub:

git add .
git commit -m "step 3 done"
git push origin master

Then create a branch, which will serve as a snapshot of the current state of your project, by running these commands:

git branch step3-complete
git push origin step3-complete

Step 4 - 404 Page and Installing Prism

Page Not Found (404 page)

Let's now work on a 'page not found' page. Add this route to app.js:

app.get("/404", (req, res) => {
  res.status(404);
  res.render('default-layout', {
     title: "Page Not Found",
     content: "<h1>Sorry!</h1><h3>We can't find the page you're requesting.</h3>"
  });
});

Now navigate to the route in the browser (localhost:8080/404), and you should see the 'page not found page'

If you navigate to this URL: http://localhost:8080/some-page/ you'll see the the server responds with a message that says 'Cannot GET /some-page/'. This is because there we have not defined a route that matches the request.

We can add a 'wildcard' route that will match any request and redirect to the 404 page, but we need to make sure that it comes after all of the other routes in app.js.

Add this code after all the other routes in app.js:

app.all('*', (req, res) => {
  res.status(404).redirect("/404");
});

Note that we are calling the all() method of the Express application object, which means that the route will respond to all types of requests (GET, POST, etc.). Also note that the first parameter is an asterisk, known as the 'wildcard' character, which means that it will match any requested route (this is why we need to make sure that this route is the last one, because we need to let the other routes have a chance to match the incoming request before this one gets used). Now navigate to the bogus URL again ( http://localhost:8080/some-page/ ), and you should see that you get redirected to the 'page not found' page.

Installing Prism

We'll use Prism JS to display the code samples in our blog pages.

Go to this page to configure and download the Prism javascript and css files.

  1. Choose a theme
  2. Choose the languages that you'll be using for your code samples (you should leave the default ones selected, but you may want to add additional ones).
  3. Make sure to include the Unescaped Markup and Normalize Whitespace plugins.
  4. At the bottom of the page, you should see links to download both the .js and .css files after you've made your configuration settings.
  5. Create a folder named prism inside the public folder and put the downloaded files in this folder.
  6. Add the following HTML code to the top.ejs template file (put it inside the HEAD element):
<link rel="stylesheet" href="/prism/prism.css">
<script src="/prism/prism.js"></script>

Finally, check out the sample blog page ( http://localhost:8080/blog/sample-blog-page/ ) and note that the code JavaScript code sample is using Prism.

At this point, you should commit your changes and push the main/master branch to GitHub:

git add .
git commit -m "step 4 done"
git push origin master

Then create a branch, which will serve as a snapshot of the current state of your project, by running these commands:

git branch step4-complete
git push origin step4-complete

Step 5 - Finishing the Contact page

Currently, when you submit the contact form in the browser, the server will respond with a 'TODO' message. Now we'll begin to replace that message with code that sends you an email. The email sent by the app will notify you that someone has submitted the form, and it will include the data that was entered into the contact form.

The email will be sent from your cPanel account, so you'll need your cPanel username and password to complete this step.

We'll take this in small steps, and test the code as we progress.

Start out by updating the route that handles POST requests to /contact/submit so that it looks like this:

app.post('/contact/submit', (req, res) => {
  
  // Extract the form input from the request body:
  const firstName = req.body.firstName;
  const lastName = req.body.lastName;
  const email = req.body.email;
  const comments = req.body.comments;
  // Note: You could extract the form input with a single line that 'de-structures' the request body:
  //const {firstName, lastName, email, comments} = req.body;
  
  // Make sure none of the variables are empty (falsy)
  if(firstName && lastName && email && comments){
    res.send("TODO: more validation and send an email");
    // TODO: make sure the email entered into the form is a valid email address
    // TODO: make sure the form does not include spam
    // TODO: send an email to YOUR email address with the data entered into the form
  }else{
    res.status(400).send("Invalid request - there is at least one empty input in the form")
  }

});

Then the IF statement checks those variables to make sure none contain an empty string (which would evaluate to 'falsy').

If any of the variables are falsy, then we end up inside the ELSE block, and send a 400 status code in the response, which indicates that there is at least one empty input in the form.

Try this out by navigating to the contact page, and submitting the form without providing any input.

Then try it again, this time enter something into each of the form inputs. When you submit the form this time, you should see the TODO message that says we need to do more validation and send an email.

In order to take care of the 3 TODO items that are commented in the code, we'll create a module.

Create a file in the modules folder named contact-helpers.js, and put this code into it:

// validates an email address (returns true it is valid, false if it is not)
function isValidEmailAddress(email){
  // a regular expression that checks if a string matches the pattern of an email address.
  var regExp = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
  return regExp.test(email);
}

// checks a string to see if a URL is in it (returns true if the string has a URL in it, false if not)
// (if there is a URL in the form data, then we consider it to be SPAM!)
function containsURL(str){
  // a regular expression that checks if a string contains a URL
  var regExp = /\b(?:(?:https?|ftp):\/\/|www\.)[-a-z0-9+&@#\/%?=~_|!:,.;]*[-a-z0-9+&@#\/%=~_|]/i;
  return regExp.test(str);
}

function isValidContactFormSubmit(firstName, lastName, email, comments){
  // make sure none of the params are empty
  if(firstName && lastName && email && comments){
    // make sure no SPAM has been included in the form
    if(containsURL(firstName) || containsURL(lastName) || containsURL(comments)){
      return false;
    }
    // make sure the email address is valid
    if(!isValidEmailAddress(email)){
      return false;
    }
    // if everything passes validation, then return true
    return true;
  }
  
  return false;
  
}

function sendEmailNotification(message, callback){
  // TODO: we'll work on this function later
}

exports.isValidContactFormSubmit = isValidContactFormSubmit;
exports.sendEmailNotification = sendEmailNotification;

Here are some things to note about the code for the contact-helpers module:

  1. The isValidEmailAddress() function returns true if its parameter is a valid email address (it's using a regular expression to check the parameter). Otherwise it returns false. We've already used this function in one of our class activities.
  2. The containsURL() function uses a regular expression to see if the parameter contains a URL. We don't allow any input that contains a URL because it could be SPAM or a phishing attack.
  3. The isValidContactFormSubmit() function makes use of the two previous functions by feeding the parameters into them. It will return true if all the parameters are valid, and false if any of them are not valid.
  4. The isValidEmailAddress() and containsURL() functions are NOT exported, which means that they are private and can only be used (invoked/called) from inside the contact-helpers module.
  5. The isValidContactFormSubmit() and sendEmailNotification() function ARE exported, and we'll import them into app.js next.

Now, in app.js, update the route for POST requests to /contact/submit to look like this:

app.post('/contact/submit', (req, res) => {
  
  // import the contact-helper functions that we need
  const {isValidContactFormSubmit, sendEmailNotification} = require("./modules/contact-helpers");

  // Extract the form input from the request body:
  const firstName = req.body.firstName;
  const lastName = req.body.lastName;
  const email = req.body.email;
  const comments = req.body.comments;
  // Note: You could extract the form input with a single line that 'de-structures' the request body:
  //const {firstName, lastName, email, comments} = req.body;
  
  // Validate the variables
  if(isValidContactFormSubmit(firstName, lastName, email, comments)){
    res.send("TODO: Everything is valid, so send an email to my email account");
  }else{
    res.status(400).send("Invalid request - input is not valid");
  }

});

Now we'll test the contact form by doing the following:

  1. Submit the form without entering any data into it - you should see the 'Invalid request' message
  2. Submit the form by entering data in all inputs, but enter an invalid email address - you should see the 'Invalid request' message
  3. Enter data into all inputs, and make sure the email address IS valid, but add a URL to any of the other inputs - you should see the 'Invalid request' message
  4. Submit the form by entering all valid data into the inputs - you should see the message that says "TODO: Everything is valid, so send an email to my email account".

Finally, we can get started on the code for the sendEmailNotification() function. This is quite complicated and will require you to use email settings from your Western Web Hosting plan.

We'll also use an NPM package named nodemailer that can send emails. When the contact form is submitted, the server will send an email to your Western Hosting Account email address.

Install the nodemailer package by running this command:

npm install nodemailer

Now update the sendEmailNotification() function (in the contact-helpers module) so that it looks like this (this is complicated code, so you don't need to understand all of it):

function sendEmailNotification(message, callback){
  
  // import the node mailer package
  const nodemailer = require('nodemailer');

  const DOMAIN = "YOUR DOMAIN GOES HERE" // ex: mywebsite.com
  const EMAIL_SERVER = "mail." + DOMAIN;
  const EMAIL_ADDRESS = "_mainaccount@" + DOMAIN;
  const EMAIL_PASSWORD = "YOUR cPanel PASSWORD GOES HERE";
  
  // create reusable transporter object using the default SMTP transport
  let transporter = nodemailer.createTransport({
    host: EMAIL_SERVER,
    port: 465,
    secure: true,
    auth: {
      user: EMAIL_ADDRESS, 
      pass: EMAIL_PASSWORD, 
    },
  });

  const email = {
    from: EMAIL_ADDRESS,
    to:EMAIL_ADDRESS,
    subject: 'Contact Submit From Your Website',
    text: message
 };
  
 transporter.sendMail(email, callback);
}

In order for this code to work, you will have to provide some information based on your Western hosting account:

  1. Make sure to set the DOMAIN constant to your website domain (for example, my domain is 'webcoder.club').
  2. Set the EMAIL_PASSWORD constant by using your cPanel password.

Note that the sendEmailNotification() function is asynchronous, and requires you to pass in a callback function as a parameter. This function will get called when the email is sent successfully, or when an error occurs when sending the email.

Side note

It's not a good idea to put sensitive information, such as your email account password directly into your code. Instead you should store sensitive information in environment variables. We won't be using environment variables in this project, but if you're interested, check out the dotenv package (it's an NPM package that allows you to set and use environment variables).

Now we can update the route that handles the POST requests to /contact/submit to look like this:

app.post('/contact/submit', (req, res) => {
  
  // import the helper functions that we need
  const {isValidContactFormSubmit, sendEmailNotification} = require("./modules/contact-helpers");

  // Destructure the req.body object into variables
  const {firstName, lastName, email, comments} = req.body;
  
  // Validate the variables
  if(isValidContactFormSubmit(firstName, lastName, email, comments)){
    // Everything is valid, so send an email to YOUR email address with the data entered into the form
    const message = `From: ${firstName} ${lastName}\n
                    Email: ${email}\n
                    Message: ${comments}`;

    sendEmailNotification(message, (err, info) => {
      if(err){
        console.log(err);
        res.status(500).send("There was an error sending the email");
      }else{
        // TODO: render the confirmation page (but for now we'll just send the 'info' param to the browser)
        res.send(info);
      }
    })
    
  }else{
    res.status(400).send("Invalid request - data is not valid")
  }

});

Note the following about this code:

  1. We are constructing a 'message' string from the data that was posted in the form.
  2. We are calling the sendEmailNotification() function and passing in the message, and a callback function.
  3. The code in the body of the callback function checks to see if an error occurred (if the err parameter is truthy).
  4. If there is no error, we send the 'info' param in our response to the browser. This 'info' object will contain information about the email that was sent.

Assuming that you've set the constants correctly, and everything on your Western Hosting account is working, you should be able to submit the contact form (with valid data entered into the form) and see a message that looks something like this (this is the 'info' object):

{"accepted":["_mainaccount@webcoder.club"],"rejected":[],"ehlo":["SIZE 52428800","8BITMIME","PIPELINING","PIPECONNECT","AUTH PLAIN LOGIN","HELP"],"envelopeTime":140,"messageTime":78,"messageSize":404,"response":"250 OK id=1rnpSk-009HYE-2O","envelope":{"from":"_mainaccount@webcoder.club","to":["_mainaccount@webcoder.club"]},"messageId":"<afe04bcd-1731-8784-be25-99ca162189aa@webcoder.club>"}

Also, if you look at your default email account for your Western Hosting, you should see that an email is in your inbox!

Once you've confirmed that you are receiving the emails that are sent by the application, you can finish things off by updating the call to sendEmailNotification() (in app.js) so that it renders a template instead of sending the 'info' object in the response, like so:

sendEmailNotification(message, (err, info) => {
  if(err){
    console.log(err);
    res.status(500).send("There was an error sending the email");
  }else{
    // Render a template that confirms the contact form info was recieved:
    res.render("default-layout", {
      title: "Contact Confirmation",
      content: "<h2>Thank you for contacting me!</h2><p>I'll get back to you ASAP.</p>"
    })
  }
});

Redirecting to HTTPS on the live server

On the live server, we need to ensure that visitors come to our pages using https. Our dev environment does not support https, so we only want to enforce it when the app is running on the live environment (the live server).

We'll add a middleware function that checks the request to see if it's using https, and if not, redirects to the https version of the requested URL.

Put this code in the MIDDLEWARE section of app.js:

// Redirect to HTTPS
app.use((req, res, next) => {
   if (process.env.NODE_ENV === 'production') {
      if (req.headers['x-forwarded-proto'] !== 'https')
         // the statement for performing our redirection
         return res.redirect('https://' + req.headers.host + req.url);
      else
         return next();
   }else{
      return next();
   }
});

At this point, you should commit your changes and push the main/master branch to GitHub:

git add .
git commit -m "step 5 done"
git push origin master

Then create a branch, which will serve as a snapshot of the current state of your project, by running these commands:

git branch step5-complete
git push origin step5-complete

Optional - Make the Blog List Page the Home Page

A really good home page requires a lot of thought. If you are interested in learning more about designing a home page, checkout this article. If you don't have any specific ideas for what to do with your homepage, you can redirect it to the blog list page. To do so, update the route for the homepage to look like this:

app.get('/', (req, res) => {
  // res.render('home', {
  //   title: "My Home Page"
  // });
  res.redirect("/blog");
});

This will effectively make your blog list page the homepage for your site. If you decide to do this, then you should also remove the 'Blog' link from the nav bar.

Optional - Running the App in a Docker Container

You have been learning about containers in the Dev Ops class. Running apps in containers are an extremely popular way to deploy apps, because containers are very light-weight and they ensure a consistent environment.

Before containers, many apps were run on virtual machines. Virtual machines were great because you could split one physical server into multiple VMs, each with its own operating system. This allowed for running multiple apps on a single physical computer.

But containers are even more efficient than VMs because multiple containers can run on a single operating system (as long as Docker is installed on the OS).

Here are the steps to run your final project in a Docker container (you need to have Docker installed on your computer).

Create a file named Dockerfile in your project folder (note that the Docker file does not have a file extension). Then put this into the file:

FROM node:20
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD node app.js

This file instructs Docker to pull an image from the Docker repository that has NodeJS (version 20) installed. Then it copies the package.json file into the container (which has a listing of all the NPM packages that will need to be installed on the container). It then runs the command to install all those packages (npm install). After all the packages are installed, it copies all the files from your project folder into the container. Finally, it runs the command that launches the app (node app.js)

The next step is to 'build an image' from the Docker file. To do so, run this command (run it from the project folder):

docker build -t web2-final-img .

You can see a list of all the images that are on you computer by running this command:

docker images

You should see an image named web2-final-img.

Now your can create and run a container from the image by entering this command:

docker run -d --name web2-final-container -p 8081:8080 web2-final-img

This command creates a container from the web2-final-img and names it web2-final-container. It also 'exposes' port 8080 in the container to port 8081 on your (host) computer. This allows you to see the app running from your local (host) computer by opening a browser and navigating to localhost:8081.

To see all the containers that are currently running on your (host) computer, enter this command:

docker ps

You should see the web2-final-container in the list.

You can stop the container with this command:

docker stop web2-final-container

If you no longer need the container, you can remove it like so:

docker rm web2-final-container

And if you no longer need the image, you can remove it with this command:

docker rmi web2-final-img