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
└── ...
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}`);
});
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;
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
└── ...
This file does a few things:
- On startup
- Connect to the database called
./data.db
, or create it if it doesn't exist - Create a
Posts
table if it doesn't exist - Log the contents of the
Posts
table if it does exist
- Connect to the database called
-
getLikesByPostId
is self-explanatory - it gets thelikeCount
for a givenpostId
, and if the post doesn't have a record in the database yet, it creates that row and sets thelikeCount
value to0
-
likePost
increments thelikeCount
for a givenpostId
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 };
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;
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.
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;
});
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
└── ...
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 #}
...
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
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);
});
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>
...and add this line anywhere in your .eleventy.js
config too, so this file is included in the build:
eleventyConfig.addPassthroughCopy("./src/scripts/");
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!)
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!
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>
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");
}
});
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"
},
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)