memes with no uploading Link to repo: https://github.com/jacobwicks/memeMaker

App on Heroku: https://my-meme-maker.herokuapp.com

Project Goals

We are going to make a web server that will let users create memes without uploading anything.

First, we’ll make a route on the server that will generate an image containing the requested path as text eg. server/text/hello world will return a jpg that contains the text “hello world”

hello world

Then we’ll make another route that takes both text and a hosted image address and makes a meme.

e.g. server/meme/hello world/https://cdn2.thecatapi.com/images/afk.jpg/ will return a picture of a kitten (hosted at https://cdn2.thecatapi.com/images/afk.jpg/) with the text “hello world” written on it.

We’ll use JavaScript, express, and node-canvas to make the code work. We’ll use GitHub and Heroku to put it online.

You’ll need to have node and npm installed. The code uses some newer features of node, so make sure you have node v.14.3 or higher.

You should also have nodemon installed. Nodemon lets you run your code and refreshes it every time you save changes. You can skip installing nodemon and just use node instead, but you’ll have to manually restart your code.

If you want to host your meme server online, you will also need a GitHub account and a Heroku account. You will also need the Git cli installed on your computer.

Get Started

Make a new folder. Name it whatever you want. I named my folder memeMaker. In your new folder, run npm init -y to initialize the project.

$ npm init -y

Install express. express will let us make a web server. A web server accepts requests from web browsers and sends data back. The web server will let users request memes, run the code to create the memes, and send the meme files as a response to the request.

$ npm i express

Install node-canvas. node-canvas is a version of the HTML canvas API that runs in node instead of in the browser. The canvas API lets you use JavaScript to create and change images and text. We’ll start by using canvas to make a .jpg that contains text. Then later we’ll write the code that loads an image into the canvas before drawing text on top of it.

$ npm i canvas

Make the Web Server

Ok, now we are ready to write some code. Create a new file named index.js. Write this code in it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
blockName: firstExpressServer
//get the express library
const express = require("express");

//the web server
const app = express();

//the port that the server will listen on
const port = 8081;

//this is a 'route'
//it defines the response to an http 'get' request
app.get("/", (req, res) =>
  //this response will display text in the browser
  res.send("You have reached the Meme Maker")
);

//start the web server listening
app.listen(port, () => {
  console.log(`Meme Maker listening at on port ${port}`);
});

Start the server.

$ nodemon index

You should see this:

server running

Now you can reach your server by going to http://localhost:8081/

serving on localhost

Use Canvas to Make an Image From Text

Ok, now let’s write the function that creates an image from an input string.

Put this code in at the top of the index.js file.

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
40
41
42
43
44
45
46
47
blockName: makeTextImage
//createCanvas is the function that creates the canvas object
const { createCanvas } = require('canvas');

//accepts an input string
//returns an image of the input text as a buffer
const makeTextImage = (input) => {
  //creates the html canvas object
  //with a width of 200px
  //and a height of 200px
  const canvas = createCanvas(200, 200);

  //a reference to the 2d canvas rendering context
  //used for drawing shapes, text, and images
  const context = canvas.getContext("2d");

  //the font we are using
  const fontSetting = "bold 50px Impact";

  //set context to use the fontSetting
  context.font = fontSetting;

  //context.measureText is a function that measures the text
  //so we can adjust how wide the finished image is
  const textWidth = context.measureText(input).width;

  //change the canvas width to be wider than the text width
  canvas.width = textWidth + 100;

  //changing canvas width resets the canvas, so change the font again
  context.font = fontSetting;

  //fillStyle sets the color that you are drawing onto the canvas
  context.fillStyle = "white";

  //fillText draws text onto the canvas
  context.fillText(input, 50, 50, textWidth + 50);

  //set the color to black for the outline
  context.fillStyle = "black";

  //strokeText draws an outline of text on the canvas
  context.strokeText(input, 50, 50, textWidth + 50);

  //return a buffer (binary data) instead of the image itself
  return canvas.toBuffer();
};

The Canvas API is what lets you use JavaScript to manipulate images.

The 2d canvas context is the part of the canvas API where you load text.

This string tells canvas what font settings to use. Once you have it working try changing the font, style, and the size.

We need to measure the text and make the canvas wider than the text. If we don’t, then the text will get cut off because it is bigger than the canvas it is getting drawn on.

Drawing the meme text in white with a black outline takes more than one step. The first step is to use the Context FillText function to draw the text.

The second step for the meme text is to use the Context StrokeText function to draw the outline in black.

We want binary data to send to the browser. Just sending the whole canvas object won’t work. The Node Buffer is binary data. The Node Canvas .toBuffer function will return the canvas as a buffer.

Make a Route to Return a Text Image

Put this code in right under where you declare const port = 8081;

Users will access this route by going to your server address followed by /text/ and whatever text they want in an image. The route function will then call the makeTextImage function that you wrote earlier. It calls makeTextImage with the input string that the user typed. makeTextImage then uses the input string to make an image of the text and returns the image as binary data. The route function adds headers to the response object so the browser knows it is getting an image. Then the route function uses the response object to return the image data to the browser.

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
blockName: textRoute
//text is the route
//:input designates a parameter of the route
app.get("/text/:input", (req, res) => {
  //the ? means optional chaining
  //input will be a string equal to whatever the user types after the route
  const input = req?.params?.input;

  //call the makeTextImage function
  //and wait for it to return the buffer object
  const image = makeTextImage(input);

  //create the headers for the response
  //200 is HTTTP status code 'ok'
  res.writeHead(
    200,
    //this is the headers object
    {
      //content-type: image/jpg tells the browser to expect an image
      "Content-Type": "image/jpg",
    }
  );

  //ending the response by sending the image buffer to the browser
  res.end(image);
});

’:’ is the way to put a variable in the parameters of an express route. We named this variable ‘input’, so the route path /text is followed by :input

Optional Chaining lets you easily access nested properties of objects without causing an error if something returns undefined. Here we use it to get the input from the params property of the request object.

The headers are where the web server tells the browser what sort of response to expect. Res.writeHead lets us specify our response headers.

Res.end is where we finish writing the response and send the data to the web browser.

If you still have your server running, nodemon should have refreshed it when you saved the changes to your code.

If not, start it again by running

nodemon index

Now you can get images by going to localhost:8081/text. Try ‘hello world’ by going to localhost:8081/text/hello world.

hello world

Make a Meme: Put Text on an Image

Get the loadImage function from the canvas library.

//createCanvas is the function that creates the canvas object
//loadImage is the function that loads an image
const { createCanvas, loadImage } = require("canvas");
A word about async/await

You’ll see the words async and await in the makeMeme function. What does that mean?

The loadImage function is an async function. When you call the loadImage function, it does not immediately return the image data. loadImage has to find the image on whatever server the image is hosted on, contact the server, and wait to get the image back. So when you call loadImage, it first returns a ‘promise’ that it will eventually give you the image data. Then after loadImage has gotten the image, loadImage resolves the promise and returns the image data.

async functions give you a promise first, then the return value later

await lets us tell the code to wait for the image data before trying to do something with it. If we didn’t await for loadImage to finish, the rest of the code would try to run using the value of the unresolved promise. The unresolved promise doesn’t have the image data in it. So the rest of the code wouldn’t work.

await lets you wait for the actual return value of a function instead of just the promise

When we want to use the await keyword we have to declare that the function is async. That’s why our makeMeme function is async. makeMeme is async because makeMeme calls loadImage, and we want makeMeme to await the return value of loadImage.

you can only use await inside of async functions.

For more, go to this link: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Async_await

Write the makeMeme function.

Put the makeMeme function under the makeTextImage function but above the routes. If you need to see where, look at the complete index.js on GitHub: https://github.com/jacobwicks/memeMaker/blob/master/index.js

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
blockName: makeMeme
const makeMeme = async ({
  //the url of the image to put the text on
  url,
  //the text to put on the image
  input,
}) => {
  //if there's no image to work with
  //don't try anything
  if (!url) return undefined;

  const canvas = createCanvas(200, 200);
  const context = canvas.getContext("2d");

  const fontSetting = "bold 50px Impact";
  context.font = fontSetting;

  const text = context.measureText(input);
  const textWidth = text.width;

  //loadImage is a function from node-canvas that loads an image
  const image = await loadImage(url);

  //set the canvas to the same size as the image
  canvas.width = image.width;
  canvas.height = image.height;

  //changing the canvas size resets the font
  //so use the fontSetting again
  context.font = fontSetting;

  //do some math to figure out where to put the text
  //indent the text in by half of the extra space to center it
  const center = Math.floor((canvas.width - textWidth) / 2) | 5;
  //put the text 30 pixels up from the bottom of the canvas
  const bottom = canvas.height - 30;

  //put the image into the canvas first
  //x: 0, y: 0 is the upper left corner
  context.drawImage(image, 0, 0);

  //set the color to white
  context.fillStyle = "white";
  //draw the text in white
  //x uses the value we calculated to center the text
  //y is 30 pixels above the bottom of the image
  context.fillText(input, center, bottom);

  //set the color to black
  context.fillStyle = "black";
  //draw the outline in black
  context.strokeText(input, center, bottom);

  //return the buffer
  return canvas.toBuffer();
};

Async lets us call another function and wait for it to return a value. We use it to wait for the image to load.

Await lets call wait for the return value of a function. We use it to wait for the image to load. The image loading is asynchronous because it has to call another server to load the image.

The drawImage function is how you put an image onto the canvas. We put the image on first then draw the text on top of it.

Add a Route That Returns a Meme

Add this route right under where you declare const port = 8081;

/meme/ accepts an input string just like our /text/ route. It also accepts an image URL. Then the meme route calls the makeMeme function you just wrote. It calles makeMeme with the input string and the image URL. When makeMeme is called it loads the image from the url then uses the input string to draw the text on top of the image. The /meme route function waits for makeMeme to return the meme as a binary data buffer. Once makeMeme is done, the meme route function assigns headers to the response object so the browser knows its getting an image. Then the route sends the image data to the browser.

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
blockName: memeRoute
//this route has two parameters
//input is a string
//url* matches everything after input
app.get("/meme/:input/:url*", async (req, res) => {
  const { params } = req;
  //get the text input string from the request parameters
  const input = params?.input;


  //urls have '/' characters in them
  //but '/' is what express uses to divide up route parameters
  //so to match the whole url, we use an asterisk '*'
  //the asterisk matches everything after the first '/'
  //and assigns it to params[0]
  
  //so params.url will usually be http:
  const baseUrl = params?.url;
  //and params[0] will be www.myImageHost.com/image.jpg
  const restOfUrl = params?.[0];

  //put the baseUrl and restOfUrl together
  const url = baseUrl + restOfUrl;

  //get the image buffer
  const image = await makeMeme({ url, input });

  //create headers object
  const headers = { "Content-Type": "image/jpg" };

  //set status code and headers
  res.writeHead(200, headers);

  //end by sending image
  res.end(image);
});

makeMeme is an async function because it has to wait for the async function imageLoad to finish before it can return a value. So memeRoute uses await to wait for the return value of makeMeme before returning the image.

Now you can get memes by going to localhost:8081/meme. Try putting ‘hello world’ on this image of a kitten by going to http://localhost:8081/meme/hello%20world/https://cdn2.thecatapi.com/images/afk.jpg/.

hello world on a kitten

Great!

Host it online using GitHub and Heroku

You need to make some changes to your files before you can host them online. You need to

  • Change the port variable in index.js
  • Edit the package.json file
  • Make a new file called .gitignore

Change Port

When your app is running online, it won’t be using port 8081. It will use whatever port the server assigns to it. The server will make the assigned port available in the environment variable PORT. You can access this at process.env.PORT.

in index.js change port = 8081 to:

//the port that the server will listen on
//use the process environment variable PORT
//and if PORT is undefined, use 8081
const port = process.env.PORT || 8081;

Edit package.json

When you ran npm init, package.json was one of the files that was created. The package.json is where npm keeps track of various things about your project. To make your project work on Heroku you need to edit your package.json file.

Here’s an example of how your package.json should look when you are done.

Your package.json file has a property scripts that looks something like this:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },

Add the start script below. The start script is what tells the Heroku server to run your index file when you host your app on Heroku.

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  }

Add a new property to package.json called engines. This tells Heroku to use node 14.7. The code we wrote earlier uses features that were recently added to node. If you don’t add this line then your app will crash when Heroku tries to run it using an older version of node.

  "engines": {
    "node": "14.7"
  }

Make the .gitignore file

We are about to add the code you have written to your repository. But you just want your own code to be in your repository, not all the node modules that you are using. In your project folder on your computer, the one where you have index.js, create a .gitignore file. This file is where you tell git not to include all the software libraries you are using in your repository. Just name the file .gitignore, there is nothing in front of the extension.

Put this in your .gitignore file.

/node_modules

That’s it for the .gitignore!

GitHub

Now you need to put your code on GitHub. To put your code on GitHub you will

  • Create a Git Repository
  • Use the command line to add your code to the new repository
  • Check the repository on GitHub and see your files

Create a Git Repository

If you don’t have a GitHub account, make one here: [https://github.com/join] Make a new repository. You can name it whatever you want. I suggest memeMaker. Don’t initialize the new repository with a readme.

newRepository1

Click Create Repository. Then you will see this screen:

newRepository2

Copy the git url from this screen. The git url for my repo is https://github.com/jacobwicks/memeMaker.git. Yours will be different because you have a different GitHub username.

Add Your Code to your new Repository

Using the command line, go to the folder where you have index.js.

git initialize the directory

$ git init

Add all the files. This step is where .gitignore stops the Node Modules from being added!

$ git add .

Add a commit message.

$ git commit -m "memeMaker first commit"

This step is the most complicated step. You tell git to add a remote origin of your repository’s url.

$ git remote add origin <repository git URL>

My git url was https://github.com/jacobwicks/memeMaker.git. That’s because my git username is jacobwicks and my repository was named memeMaker.

You can copy your repository url from the screen that came up when you created your repository on git. My full command looked like this:

$ git remote add origin https://github.com/jacobwicks/memeMaker.git

Yours will look different because your username is different than mine.

Finally, push your code to your git repository.

$ git push origin master

pushToRepository

Check the Git Repo

Now check your repository on GitHub. You should see all the files you just wrote. repoOnGitHub

Great. Now we are ready to get your app running online!

Host using Heroku

One site that lets you host an express web server is Heroku.

To host on Heroku you will:

  • Create a New Heroku App
  • Connect the Heroku App to Your GitHub Repo
  • Build the App

Create a New Heroku App

You need a Heroku Account, so if you don’t have one yet go sign up.

Log in to Heroku.

Go to the Heroku dashboard. Start creating a new app by clicking the ‘New’ button then clicking ‘create new app’ in the dropdown menu. herokuDashboard

Create a new app. You can name it whatever you want. I named mine my-meme-maker. You can take whatever name is available.

herokuNewApp

Deploy by Connecting the App to GitHub

Under the deployment method, Click the ‘Connect to GitHub’ button. herokuDeploymentMethod

If this is your first time working with Heroku, you need to connect your GitHub account to Heroku.

Click the ‘Connect to GitHub’ button. This may connect your github account automatically, or there may be some more steps. herokuConnectToGitHub

After your github account is connected you will be able to search for your repo. Type the name of your repo in the search field and click the search button. My repo was named ‘memeMaker’. After the search completes, your repo will be shown below. Click the connect button. herokuSearchForRepo

Now your Heroku App is connected to your GitHub repo! herokuConnectedRepo

Build the App

Click the ‘deploy branch’ button in the Manual Deploy section. herokuBuilding

It should say “Your app was successfully deployedherokuSuccess

Click the view button and you’ll see your ‘/’ route working. hosted

Use the App!

Now you can make memes that say whatever you want! say whatever you want

Next steps

  • This code puts text at the bottom of the image. Make one that puts text at the top.
  • Let the user put text at the top and bottom of the image using more path parameters
  • The text gets cut off if it’s too long. Make it write multiple lines instead
  • Putting ‘?’ in the meme text won’t work. Make it work with question marks
  • There’s no error handling or checking for bad inputs. Add error handling and make sure the app won’t crash if it gets bad input
What to do if deployment didn’t work

That’s too bad! But this is an opportunity to learn.

To figure out what went wrong with your app, you need to look at the Heroku logs.

First, Install the Heroku CLI.

Then, using the command line, look at the logs for your app.

The command is heroku logs -a followed by your app name

heroku logs -a <your-app-name>

I called my app ‘meme-maker’ so the command to see the logs for me is this:

heroku logs -a meme-maker

Yours will be different because your app will have a different name.

Use resources like Google, Stackoverflow, documentation and forums to find the answers. Keep poking around and you will get it working.