DEV Community

Amber Joy
Amber Joy

Posted on

Adding a "Like" button to a static 11ty blog

One of my kids has a simple blog about their favorite video game. The blog uses the default Glitch 11ty blog template, and it was quick & easy to set up - took about 5 minutes. Thanks, Glitch!

As soon as the blog was deployed, things got a little more complicated. My budding blogger requested a "Like" button so they could get some positive feedback on their posts. This meant I had to add a tiny Node/Express API connected to a SQLite database which allowed us to POST and GET likes for each blog post. Let's walk through how I did it, and how you can, too!

Note: This tutorial assumes you're working with your code locally. It will work on Glitch at the end, but you won't be able to see as much progress until you update the scripts in package.json at the end!

Add a Server & API

The first step is to add Express to our project with npm install express.

Then, add a backend/ directory with a server.js file and routes/api.js

.
├── backend/
│   ├── routes/
│   │   └── api.js <--
│   └── server.js <--
├── public/
├── src/
├── .eleventy.js
└── ...
Enter fullscreen mode Exit fullscreen mode

Our server is pretty simple - it serves the static content from the build/ folder that 11ty generates, and handles a couple API routes from the routes/api.js file:

backend/server.js

const express = require("express");
const app = express();
const apiRoutes = require("./routes/api");

app.use(express.json());

app.use("/api", apiRoutes);

app.use(express.static("build"));

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

backend/routes/api.js

const express = require("express");
const router = express.Router();

router.post("/likePost", (req, res) => {
  // This is where we'll query the database in the next step!
});

router.get("/getLikes", (req, res) => {
    // ...
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

The Database

Next, we'll install sqlite3 for a database that the API will connect to.

First, install sqlite npm install sqlite3, and then create /backend/db/database.js:

.
├── backend/
|   ├── db/
|   |   └── database.js <--   
│   ├── routes/
│   └── server.js
├── public/
├── src/
├── .eleventy.js
└── ...
Enter fullscreen mode Exit fullscreen mode

This file does a few things:

  • On startup
    1. Connect to the database called ./data.db, or create it if it doesn't exist
    2. Create a Posts table if it doesn't exist
    3. Log the contents of the Posts table if it does exist
  • getLikesByPostId is self-explanatory - it gets the likeCount for a given postId, and if the post doesn't have a record in the database yet, it creates that row and sets the likeCount value to 0
  • likePost increments the likeCount for a given postId by +1

❗Make sure to add data.db to your .gitignore at this point!

backend/db/database.js

const sqlite3 = require("sqlite3").verbose();

const db = new sqlite3.Database("./data.db", (err) => {
  if (err) {
    console.error(err.message);
  } else {
    console.log("Connected to the data.db SQLite database.");
  }
});

db.serialize(() => {
  db.run("CREATE TABLE IF NOT EXISTS Posts (postId INT, likeCount INT)");

  db.each("SELECT postId, likeCount FROM Posts", (err, row) => {
    if (err) {
      console.error(err.message);
    }
    console.log("postId:", row.postId, "likeCount:", row.likeCount);
  });
});

function getLikesByPostId(postId, callback) {
  db.get(
    "SELECT likeCount FROM Posts WHERE postId = ?",
    [postId],
    (err, row) => {
      if (err) {
        console.error(err.message);
      } else if (row) {
        callback(row.likeCount);
      } else {
        db.run(
          "INSERT INTO Posts (postId, likeCount) VALUES (?, 0)",
          [postId],
          function (err) {
            if (err) {
              console.error(err.message);
            } else {
              callback(0);
            }
          },
        );
      }
    },
  );
}

function likePost(postId, callback) {
  db.run(
    "UPDATE Posts SET likeCount = likeCount + 1 WHERE postId = ?",
    [postId],
    function () {
      if (!this.changes) {
        db.run(
          "INSERT INTO Posts (postId, likeCount) VALUES (?, 1)",
          [postId],
          function (err) {
            if (err) {
              console.error(err.message);
            } else {
              callback(this.lastID);
            }
          },
        );
      } else {
        callback(this.changes);
      }
    },
  );
}

module.exports = { getLikesByPostId, likePost };
Enter fullscreen mode Exit fullscreen mode

Now we can update our two API routes to run these queries:

backend/routes/api.js

const express = require("express");
const router = express.Router();
const db = require("../db/database");

router.post("/likePost", (req, res) => {
  db.likePost(req.body.postId, (changes) => {
    res.json({ changes });
  });
});

router.get("/getLikes", (req, res) => {
  db.getLikesByPostId(req.query.postId, (likeCount) => {
    res.json({ likeCount });
  });
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Test your API

Let's test it out. Spin up your server node backend/server.js. Your output should look something like this:

λ node backend/server.js
Server running on http://localhost:3000
Connected to the data.db SQLite database.
Enter fullscreen mode Exit fullscreen mode

Now, using Postman, curl, or any API tool you like, try sending a GET request to localhost:3000/api/getLikes?postId=42. (Our database is still empty, so this will create a new record for postId: 42 since it doesn't exist.)

You should get a response of { likeCount: 0 } This means SQLite has successfully created and connected to the data.db database, created a Posts table, added a new record for postId: 42 (with 0 likes), and then returned that row data.

Great! But how do we know it's really working? Let's add a like to our imaginary postId: 42 and then see if we can GET it.

Send a POST to localhost:3000/api/likePost, and set the request body to { postId: 42 }. (Here's how to curl that if you don't use Postman: curl --location 'localhost:3000/api/likePost' \ --header 'Content-Type: application/json' \ --data '{"postId": 42}') You should get a response confirming 1 change has been posted to your database: { changes: 1 }.

Now let's try that GET again using the same request as before: localhost:3000/api/getLikes?postId=42. If everything is working correctly, you should now see a response of { likeCount: 1 }!

Now that you've confirmed your API is working to POST and GET likeCounts, let's hook it up to the view.

Show likes on the page

In .eleventy.js, add a postId data property for each post. This file already exists, we just need to add one line:

.eleventy.js

// Find this block in your file...
eleventyConfig.addCollection("posts", function (collection) {
  const coll = collection
    .getFilteredByTag("posts");

  for (let i = 0; i < coll.length; i++) {
    // There will be other content in this file,
    // but we only need to add the line below
    coll[i].data["postId"] = i;
  }

  return coll;
});
Enter fullscreen mode Exit fullscreen mode

Then, we add the postId to the existing post template. Here, we're using the postId to identify a container for the likeCount. We'll add an id of likes-container-{{postId}} and a data-post-id attribute to the container:

.
├── backend/
├── public/
├── src/
|   └── _includes
|       └──layouts
|          └──post.njk <-- 
├── .eleventy.js
└── ...
Enter fullscreen mode Exit fullscreen mode

src/_includes/layouts/post.njk

...
    {{ content | safe }}

    // Add this line below the post content
    <div id="likes-container-{{postId}}"  data-post-id={{postId}}></div>

    <div class="controls">
      {# The previous and next post data is added in .eleventy.js #}
...
Enter fullscreen mode Exit fullscreen mode

Now we're going to use good old-fashioned vanilla JS to add likes to the page. Create a new folder src/scripts/likes.js:

.
├── backend/
├── public/
├── src/
│   └── scripts/
│       └── likes.js <-- 
└── .eleventy.js

Enter fullscreen mode Exit fullscreen mode

Which should look like this to start:

src/scripts/likes.js

document.addEventListener("DOMContentLoaded", async () => {
  async function updateLikes(postId) {
    const res = await fetch(`/api/getLikes?postId=${postId}`);
    const { likeCount } = await res.json();

    if (!likeCount) {
      likesContainer.innerHTML = "0 likes ❤️";
    } else {
      const likesText =
        likeCount > 1
          ? `${likeCount.toString()} likes ❤️`
          : `${likeCount.toString()} like ❤️`;
      likesContainer.innerHTML = likesText;
    }
  }

  // Find the new likes-container
  const likesContainer = document.querySelector('[id^="likes-container-"]');

  // If we're not on a Post with a likesContainer, do nothing
  if (!likesContainer) {
    return;
  }

  // Get the postId
  const postId = likesContainer.getAttribute("data-post-id");

  // Get likes for this post
  updateLikes(postId);
});

Enter fullscreen mode Exit fullscreen mode

Import this script to your base.njk

src/_includes/layouts/base.njk

    // Add this line inside the <head>
    <script type="application/javascript" src="/scripts/likes.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

...and add this line anywhere in your .eleventy.js config too, so this file is included in the build:

  eleventyConfig.addPassthroughCopy("./src/scripts/");
Enter fullscreen mode Exit fullscreen mode

Now you can build your static blog with npm start, and serve the files by running node backend/server.js again. Even though eleventy is running on localhost:8080, we're going to view the blog at localhost:3000 since that's where the server is running and serving up our build folder. Click into any blog post, and you'll see something like this at the bottom (unless you happen to be looking at a post with ID 42, which we liked earlier!)

0 Likes

We don't have a button to add likes on the UI yet, but if you want to test displaying an updated likeCount, you can use the API to add a like to a post. The first post is easiest - postId: 0 (You can use the first of the example posts that come with the template, or delete them and add one of your own.)

Go ahead and "Like" postId: 0 with
curl --location 'localhost:3000/api/likePost' \ --header 'Content-Type: application/json' \ --data '{"postId": 0}', and now visit that post in your browser.

Voila!

1 Like

Create the button

Next, let's add a button below the likesContainer:

src/_includes/layouts/post.njk:

    <div id="likes-container-{{postId}}" data-post-id="{{postId}}"></div>

    // New button!
    <button id="like-btn-{{postId}}">
      Like this post
    </button>
Enter fullscreen mode Exit fullscreen mode

And a new event listener inside our scripts/likes.js, right below where we have updateLikes(postId):

src/scripts/likes.js

  const likeButton = document.querySelector('[id^="like-btn-"]');

  likeButton.addEventListener("click", async () => {
    const res = await fetch("/api/likePost", {
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ postId }),
      method: "POST",
    });

    if (res.ok) {
      updateLikes(postId);
    } else {
      console.error("Failed to like the post");
    }
  });

Enter fullscreen mode Exit fullscreen mode

This is going to like the selected post, and then when that call returns successfully, it makes another call to get the updated likeCount and update it on the page.

Putting it all together

Up until now, we've been running an eleventy build, and then running the server with node backend/server.js. To make running the app locally easier, and to make it deployable we'll need to make a few updates to package.json.

For local development (this does NOT work on Glitch), I like to use concurrently so I can keep my eleventy builds running with a --serve flag, and my server running with a --watch flag at the same time.

First, install concurrently: npm i concurrently

And then update the scripts object in package.json

package.json

  "scripts": {
    "start": "npm run build && npm run server",
    "dev": "concurrently \"npm run build:dev\" \"npm run server:dev\"",
    "server": "node backend/server.js",
    "build": "eleventy",
    "server:dev": "node backend/server.js --watch",
    "build:dev": "eleventy --serve"
  },
Enter fullscreen mode Exit fullscreen mode

Now we have a start script which runs an eleventy build and then starts the server. This can be used in a production deployment. We also have a dev script which runs eleventy and node concurrently so we can see updates live during development.

Test your app locally with npm run dev and opening a browser to localhost:3000. You should now be able to click "Like" and see the likeCount updated on any post in your blog!

This is a very basic functionality which allows any viewer to add as many likes as they want, but you could use localStorage to prevent this with additional logic to inform the user that they've already liked a post.

Example

Here is a sample blog that uses these updates. It looks just like the original template, but now there's a Like button on each blog post. Feel free to remix it on Glitch!

Top comments (0)