Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Add a Comments Section to an Eleventy Website with MongoDB and Netlify

TwitterFacebookRedditLinkedInHacker News

I’m a huge fan of static generated websites! From a personal level, I have The Polyglot Developer, Poké Trainer Nic, and The Tracy Developer Meetup, all three of which are static generated websites built with either Hugo or Eleventy. In addition to being static generated, all three are hosted on Netlify.

I didn’t start with a static generator though. I started on WordPress, so when I made the switch to static HTML, I got a lot of benefits, but I ended up with one big loss. The comments of my site, which were once stored in a database and loaded on-demand, didn’t have a home.

Fast forward to now, we have options!

In this tutorial, we’re going to look at maintaining a static generated website on Netlify with Eleventy, but the big thing here is that we’re going to see how to have comments for each of our blog pages.

To get an idea of what we want to accomplish, let’s look at the following scenario. You have a blog with X number of articles and Y number of comments for each article. You want the reader to be able to leave comments which will be stored in your database and you want those comments to be loaded from your database. The catch is that your website is static and you want performance.

A few things are going to happen:

  • When the website is generated, all comments are pulled from our database and rendered directly in the HTML.
  • When someone loads a page on your website, all rendered comments will show, but we also want all comments that were created after the generation to show. We’ll do that with timestamps and HTTP requests.
  • When someone creates a comment, we want that comment to be stored in our database, something that can be done with an HTTP request.

It may seem like a lot to take in, but the code involved is actually quite slick and reasonable to digest.

The Requirements

There are a few moving pieces in this tutorial, so we’re going to assume you’ve taken care of a few things first. You’ll need the following:

  • A properly configured MongoDB Atlas cluster, free tier or better.
  • A Netlify account connected to your GitHub, GitLab, or Bitbucket account.
  • Node.js 16+.
  • The Realm CLI.

We’re going to be using MongoDB Atlas to store the comments. You’ll need a cluster deployed and configured with proper user and network rules. If you need help with this, check out my previous tutorial on the subject.

We’re going to be serving our static site on Netlify and using their build process. This build process will take care of deploying either Realm Functions (part of MongoDB Atlas) or Netlify Functions.

Node.js is a requirement because we’ll be using it for Eleventy and the creation of our serverless functions.

Build a static generated website or blog with Eleventy

Before we get into the comments side of things, we should probably get a foundation in place for our static website. We’re not going to explore the ins and outs of Eleventy. We’re just going to do enough so we can make sense of what comes next.

Execute the following commands from your command line:

mkdir netlify-eleventy-comments
cd netlify-eleventy-comments

The above commands will create a new and empty directory and then navigate into it.

Next we’re going to initialize the project directory for Node.js development and install our project dependencies:

npm init -y
npm install @11ty/eleventy @11ty/eleventy-cache-assets axios cross-var mongodb-realm-cli --save-dev

Alright, we have quite a few dependencies beyond just the base Eleventy in the above commands. Just roll with it for now because we’re going to get into it more later.

Open the project’s package.json file and add the following to the scripts section:

"scripts": {
    "clean": "rimraf public",
    "serve": "npm run clean; eleventy --serve",
    "build": "npm run clean; eleventy --input src --output public"
},

The above script commands will make it easier for us to serve our Eleventy website locally or build it when it comes to Netlify.

Now we can start the actual development of our Eleventy website. We aren’t going to focus on CSS in this tutorial, so our final result will look quite plain. However, the functionality will be solid!

Execute the following commands from the command line:

mkdir -p src/_data
mkdir -p src/_includes/layouts
mkdir -p src/blog
touch src/_data/comments.js
touch src/_data/config.js
touch src/_includes/layouts/base.njk
touch src/blog/article1.md
touch src/blog/article2.md
touch src/index.html
touch .eleventy.js

We made quite a few directories and empty files with the above commands. However, that’s going to be pretty much the full scope of our Eleventy website.

Multiple files in our example will have a dependency on the src/_includes/layouts/base.njk file, so we’re going to work on that file first. Open it and include the following code:

<!DOCTYPE html>
<html>
    <head></head>
    <body onload="getCommentsForArticle()">
        {{ content | safe }}
        <div>
            <h2>Comments</h2>
            <div id="comments">
                <!-- We are not ready for this part yet! -->
            </div>
        </div>
        <div style="padding: 10px 0">
            <input id="comment_username" name="comment_username" hint="username" value="" />
            <br />
            <textarea id="comment_text" name="comment_text" hint="comment"></textarea>
            <br />
            <button onclick="createNewComment()">Create Comment</button>
        </div>
        <script>

            let encodedPageUrl = encodeURIComponent("{{ page.url }}");
            let encodedLastBuilt = encodeURIComponent("{{ config.lastBuildDate }}");

            function getCommentsForArticle() {
                fetch(`COMING_SOON`)
                .then(response => response.json())
                .then(response => {
                    response.forEach(comment => {
                        const parentTag = document.createElement("div");
                        const usernameTag = document.createElement("div");
                        const commentTag = document.createElement("div");
                        usernameTag.innerText = comment.username;
                        commentTag.innerText = comment.comment;
                        parentTag.appendChild(usernameTag);
                        parentTag.appendChild(commentTag);
                        parentTag.style.padding = "10px 0";
                        document.getElementById("comments").appendChild(parentTag);
                    })
                })
            }

            function createNewComment() {
                const username = document.getElementById("comment_username").value;
                const comment = document.getElementById("comment_text").value;
                const url = "{{ page.url }}";
                const parentTag = document.createElement("div");
                const usernameTag = document.createElement("div");
                const commentTag = document.createElement("div");
                usernameTag.innerText = username;
                commentTag.innerText = comment;
                parentTag.appendChild(usernameTag);
                parentTag.appendChild(commentTag);
                parentTag.style.padding = "10px 0";
                document.getElementById("comments").appendChild(parentTag);
                fetch("COMING_SOON", {
                    "method": "POST",
                    "body": JSON.stringify({
                        "username": username,
                        "comment": comment,
                        "url": url
                    })
                })
                .then(response => response.json())
                .then(response => {
                    document.getElementById("comment_username").value = "";
                    document.getElementById("comment_text").value = "";
                });
            }
        </script>
    </body>
</html>

Alright, so the above file is, like, 90% complete. I left some pieces out and replaced them with comments because we’re not ready for them yet.

This file represents the base template for our entire site. All other pages will get rendered in this area:

{{ content | safe }}

That means that every page will have a comments section at the bottom of it.

We need to break down a few things, particularly the <script> tag at the bottom of the page.

In the <script> tag, we have two variables and two functions.

let encodedPageUrl = encodeURIComponent("{{ page.url }}");
let encodedLastBuilt = encodeURIComponent("{{ config.lastBuildDate }}");

The above two variables hold the current page URL, which will be important for identifying which page should have which comments as well as the last build date for our Eleventy website. The page.url variable is part of Eleventy, but we’ll be creating our config.lastBuildDate variable in a moment.

Knowing the last build date of our website is important for pulling only the comments we need for any given page rather than all comments, but more on that soon.

Next we have a function that looks like this:

function getCommentsForArticle() {
    fetch(`COMING_SOON`)
    .then(response => response.json())
    .then(response => {
        response.forEach(comment => {
            const parentTag = document.createElement("div");
            const usernameTag = document.createElement("div");
            const commentTag = document.createElement("div");
            usernameTag.innerText = comment.username;
            commentTag.innerText = comment.comment;
            parentTag.appendChild(usernameTag);
            parentTag.appendChild(commentTag);
            parentTag.style.padding = "10px 0";
            document.getElementById("comments").appendChild(parentTag);
        })
    })
}

Remember, our comments are going to be stored externally. For this reason, we need to be doing HTTP requests. Ignoring the endpoint URL for now, we’re expecting an array of comments to come back from the request. In the above function, we loop through the comments and create a new tag to be injected into our HTML for each comment in the array. While we’ll be storing more information, we’re only presenting the username and comment on the screen.

Creating comments is similar in design:

function createNewComment() {
    const username = document.getElementById("comment_username").value;
    const comment = document.getElementById("comment_text").value;
    const url = "{{ page.url }}";
    const parentTag = document.createElement("div");
    const usernameTag = document.createElement("div");
    const commentTag = document.createElement("div");
    usernameTag.innerText = username;
    commentTag.innerText = comment;
    parentTag.appendChild(usernameTag);
    parentTag.appendChild(commentTag);
    parentTag.style.padding = "10px 0";
    document.getElementById("comments").appendChild(parentTag);
    fetch("COMING_SOON", {
        "method": "POST",
        "body": JSON.stringify({
            "username": username,
            "comment": comment,
            "url": url
        })
    })
    .then(response => response.json())
    .then(response => {
        document.getElementById("comment_username").value = "";
        document.getElementById("comment_text").value = "";
    });
}

In the above code, we are doing three things:

  • Extract the comment information from the form.
  • Inject the comment information into the HTML like the previous function.
  • Send the comment information to our HTTP endpoint.

It may look like a lot, but most of the code is related to injecting HTML. The heavy lifting is done by the endpoint that we’ll handle later.

So I mentioned the last build date. Let’s check that out. Open the project’s src/_data/config.js file and include the following:

module.exports = {
    lastBuildDate: new Date()
};

We have to remember that when we work with Eleventy, we’re working with both client JavaScript and Node.js. We don’t want the client to generate timestamps because that doesn’t properly reflect the build time. Instead, we can use this configuration file that is used at build time instead.

So we have the base template for our website. Let’s make the child pages and have them use it.

Open the project’s src/index.html file and include the following code:

---json
{
    "layout": "layouts/base.njk"
}
---

<h1>Netlify with MongoDB Comments Example</h1>

<ul>
    {% for post in collections.posts %}
        <li><a href="{{ post.url | url }}">{{ post.url }}</a></li>
    {% endfor %}
</ul>

Because we’re defining a layout, anything on this page will get injected into the appropriate part of that template. The purpose of this page is just to show each of our blog pages.

Now open the project’s src/blog/article1.md file:

---json
{
    "layout": "layouts/base.njk"
}
---

This is a **FIRST** article. The important piece for this example is the URL.

Not too much different than the HTML page, but this time, we’re using Markdown—something we’d use for writing blog articles.

Finally, we have our other blog page in the src/blog/article2.md file:

---json
{
    "layout": "layouts/base.njk"
}
---

This is a **SECOND** article. The important piece for this example is the URL.

I realize we created three files that are more or less the same, but they are necessary to prove our point. We are going to have a multiple page static generated site and the comments should only appear on the correct page of that site.

Alright, so on the src/index.html page, we had a loop for our blog posts. This is great, but we need to define that our blog posts can be looped. For this, we need to create an Eleventy configuration file beyond the build-related configuration file that we previously defined.

Open the .eleventy.js file at the root of your project and add the following code:

module.exports = function (eleventyConfig) {

    eleventyConfig.addCollection("posts", (collection) => {
        return collection
            .getFilteredByGlob("src/blog/**/*.md");
    });

    eleventyConfig.addFilter("getCommentsForUrl", (comments, url) => {
        let filtered = comments.filter(entry => entry["url"] === url);
        return filtered;
    });

    return {
        dir: { input: "src", output: "public" }
    };

};

We added a little more to this file than we need at the moment, but I’ll explain it while we’re here.

First, you’ll notice the following:

eleventyConfig.addCollection("posts", (collection) => {
    return collection
        .getFilteredByGlob("src/blog/**/*.md");
});

The above snippet says that all Markdown files in the src/blog directory and child directories should be added to an Eleventy collection called posts that can be looped over.

Then we have the following:

eleventyConfig.addFilter("getCommentsForUrl", (comments, url) => {
    let filtered = comments.filter(entry => entry["url"] === url);
    return filtered;
});

The above filter says that when we loop over something in our project, we can filter to only return results that match our URL in this case. This will be useful when showing only comments that should be shown for a particular page. Remember, we know the page URL and that URL will also be stored in MongoDB for any particular comment.

With the exception of this mysterious src/_data/comments.js file and our placeholder endpoint URLs, the Eleventy site is done! We’ll circle back to the other outstanding items when we need them.

Now we need to figure out how we want to engage with MongoDB to obtain those comments and work with them. For this example, we’re going to explore two options, going all in with MongoDB by using Realm or Netlify Functions.

Create API endpoints for comment interaction with Realm and MongoDB Atlas

We’re not following any particular order here, but we’re going to start with Realm for interacting with our comment data. This will be modular and you can choose to use it or not.

To be successful, you’re going to need to have API keys in hand for working with Realm from the CLI—something we plan to do locally and part of the Netlify build process.

Within the MongoDB Cloud, create a new Realm application and tell it which cluster to use. Next, you’ll want to click the ellipsis menu next to the cluster name at the top left of the screen.

Realm Project Settings

Click the “Project Settings” link followed by “Access Manager.” When you create a new API key, you’ll want to give it “Project Owner” permissions. The name of this key is not important, but it should be a name meaningful to you.

Save both the public key and private key because we’ll need to enter them in two places, one for the local CLI and one for Netlify.

Let’s log into the Realm CLI. Execute the following from your command line:

realm-cli login

Enter the keys when prompted. You can also add them as environment variables and do something like this instead:

realm-cli login --api-key=$REALM_API_KEY --private-api-key=$REALM_PRIVATE_API_KEY

After you log into the Realm CLI successfully, we need to pull down our Realm project into our Eleventy project.

At the root of your Eleventy project, execute the following:

realm-cli pull --local realm

You should be prompted to choose which Realm project you want to use. Select the correct project and it will be downloaded to a realm directory within your project.

With Realm, HTTP endpoints rely on Realm Functions. There’s also some configuration on top of this, but our bottom layer is the Realm Functions. We’re going to start from the bottom and work our way to the top.

Create the following files with the command line:

touch realm/functions/create_comment.js
touch realm/functions/get_comments.js
touch realm/functions/config.json

We’re going to start by working on the function for creating our comment within MongoDB.

Open the realm/functions/create_comment.js file and include the following:

exports = function({ query, headers, body}, response) {
    let payload = EJSON.parse(body.text());
    payload.timestamp = new Date();
    const result = context.services.get("mongodb-atlas").db("netlify").collection("comments").insertOne(payload);
    return result;
};

The above function will take a request payload and parse it. Next, we’ll take the current timestamp information and insert it into MongoDB.

For this example, we’re not doing any data validation. Take this into consideration when attempting to take this material to the next level.

Let’s look at our function for obtaining comments. Open the project’s realm/functions/get_comments.js file and add the following:

exports = async function({ query, headers, body}, response) {
  
    const commentFilter = {};
    
    if(query.hasOwnProperty("url")) {
        commentFilter["url"] = decodeURIComponent(query.url);
    }

    if(query.hasOwnProperty("last_built")) {
        commentFilter["timestamp"] = {
            "$gt": new Date(decodeURIComponent(query.last_built))
        }
    }
    
    const results = await context.services.get("mongodb-atlas").db("netlify").collection("comments").find(commentFilter).toArray();

    response.setHeader("content-type", "application/json");
    response.setBody(JSON.stringify(results));

};

The above code is slightly more extravagant.

In the above code, we are looking at the query parameters of the request. We want the URL, which represents our page URL, and we want the last build date of our Eleventy website. Both of these query parameters are optional but will play an important role in what’s to come.

Using those optional parameters, we can do a query and return the results.

For both the get_comments.js and create_comments.js functions, we are using a “netlify” database and “comments” collection within MongoDB. Neither need to exist prior to runtime as they will be created if they don’t exist.

With the function logic in place, let’s configure those functions. Open the project’s realm/functions/config.json file and add the following:

[
    {
        "name": "get_comments",
        "private": false,
        "run_as_system": true
    },
    {
        "name": "create_comment",
        "private": false,
        "run_as_system": true
    }
]

In the above configuration, we are defining the levels of permissions they should use. Take a moment to brush up on the type of roles you can use for Realm Functions as my choice in roles might not be the best for you.

The functions are good to go, but now we need to configure HTTP endpoints to interact with them.

Create a realm/http_endpoints/config.json file if it doesn’t exist and add the following:

[
    {
        "route": "/comment",
        "http_method": "POST",
        "function_name": "create_comment",
        "validation_method": "NO_VALIDATION",
        "respond_result": true,
        "fetch_custom_user_data": false,
        "create_user_on_auth": false,
        "disabled": false
    },
    {
        "route": "/comments",
        "http_method": "GET",
        "function_name": "get_comments",
        "validation_method": "NO_VALIDATION",
        "respond_result": true,
        "fetch_custom_user_data": false,
        "create_user_on_auth": false,
        "disabled": false
    }
]

What we’re doing is we’re defining the HTTP endpoint URL route to a particular function. We’re also specifying if data should be returned, among other things. Your best bet is to look into the documentation if you need a more thorough explanation on any of the fields.

The Realm side of things should be good to go. Try deploying the HTTP endpoints with the following:

cd realm
realm-cli push

If you want to interact with any of your HTTP endpoints, you should be able to do something like this:

curl https://us-east-1.aws.data.mongodb-api.com/app/$REALM_APP_ID/endpoint/comments

Just add “$REALM_APP_ID” as an environment variable with your own application ID. Remember that we have a GET endpoint as well as a POST endpoint.

Let’s wire these functions into our Eleventy website.

Start by opening the project’s src/_includes/layouts/base.njk file and slipping the Realm HTTP endpoint URLs into the correct fetch operations.

If you did nothing else, you’d end up with comments on your site when people visit it. However, all comments from all pages would end up on every page. Let’s fix that with a query parameter.

fetch(`https://us-east-1.aws.data.mongodb-api.com/app/$REALM_APP_ID/endpoint/comments?url=${encodedPageUrl}`)

Remember, you have the page URL in the base.njk file. We just needed to pass it with our fetch operation to the endpoint for retrieving comments. Just like that, you’re only getting comments for the page you’re on.

We can do better though!

Right now, all comments for a particular page are loaded at runtime. While it’s asynchronous, retrieving all comments could be slow if you have a lot of comments. Remember, one of the main selling points of static generated websites is that they are fast.

Let’s modify our project to pre-load our comments and render them directly into the HTML prior to deployment.

Open the project’s src/_data/comments.js file and add the following:

const { AssetCache } = require("@11ty/eleventy-cache-assets");
const axios = require("axios");

module.exports = async function () {

    const REALM_APP_ID = "<YOUR_APP_ID>";

    const url = `https://us-east-1.aws.data.mongodb-api.com/app/${REALM_APP_ID}/endpoint/comments`;

    let asset = new AssetCache("comments");

    if (asset.isCacheValid("1d")) {
        return asset.getCachedValue();
    }

    try {
        const response = await axios({
            "method": "GET",
            "url": url
        });
        await asset.save(response.data, "json");
        return response.data;
    } catch (error) {
        console.error(error);
        return null;
    }

}

We’re doing two big things in the above chunk of code:

  • We’re caching the comments locally between website builds.
  • We’re obtaining access to comments at build time rather than just runtime.

So what does this mean?

Every time we build our website or every time we hit the save button while serving our website, Eleventy will try to request all comments from our function. If you’re like me, you hit the save button every half a second, so this would be a lot of function calls. To get beyond this, we cache those requests and read from the cache until it expires.

Whether we need to update the cache or make a request, we’re going to have access to all the comments by the time this runs.

To get the comments pre-loaded into our HTML, let’s circle back to the src/_includes/layouts/base.njk file and add the following in the HTML:

<div>
    <h2>Comments</h2>
    <div id="comments">
        {% for comment in (comments | getCommentsForUrl(page.url)) %}
            <div id="comment-{{ comment._id }}" style="padding: 10px 0">
                <div>{{ comment.username }}</div>
                <div>{{ comment.comment }}</div>
            </div>
        {% endfor %}
    </div>
</div>

In the above HTML, we are looping through the comments array of data. Remember in Eleventy, the variable is the same name as the file in the _data directory. However, we don’t want to render all comments, so we use the getCommentsForUrl filter that we added to the .eleventy.js file. This filter will take the current page URL and filter out only the comments we want.

Remember the <script> tag at the bottom of this file? Both functions in that tag will look for the comments element id when injecting HTML on the fly. So we can still have both pre-loaded comments as well as on-demand comments.

So how do we prevent duplicates?

Remember the last recorded build time? We can use that to get only the difference in comments. To be clear, the pre-loading should get all comments for any given URL. The on-demand comments should be all comments after the build date. This means we’re getting only the comments that haven’t been baked into the HTML.

Let’s change our fetch URL a bit more:

fetch(`REALM_URL_HERE/comments?url=${encodedPageUrl}&last_built=${encodedLastBuilt}`)

This will work pretty slick. If you build every day like I do, the amount of on-demand comments through client-side HTTP requests will be minimal, giving you the best possible performance without missing any of the conversation.

Our Eleventy website should work fine locally with Realm and MongoDB at this point. We’ll explore deployments later in this tutorial.

Develop API endpoints with Netlify Functions and the MongoDB Atlas Data API

You just saw our first possible approach to comments in an Eleventy website. Realm is a perfectly suitable option and is probably a lot easier than I lead you to believe in this tutorial. However, we have another option. We can abandon Realm and use Netlify Functions for the job as well.

To get us where we need to be, we’re going to use Netlify Functions and the MongoDB Atlas Data API. To be clear, you could install the MongoDB Node.js driver and use it in a Netlify Function, but the Data API is so much easier for this scenario!

While you don’t need the Netlify CLI for what comes next, you will get good value out of it for testing things locally. To install the Netlify CLI, execute the following command from the command line:

npm install -g netlify-cli

It’s up to you if you want to sign into your Netlify account from the CLI. It’s not absolutely necessary.

From the project directory, execute the following:

netlify functions:create --name create_comment
netlify functions:create --name get_comments

Use the JavaScript language and the “Hello World” template when prompted. At the end of this, you’ll find a new functions directory in your project with the new function files.

It’s possible you’ll need a netlify.toml configuration file in place prior to creating these functions. You can create one with the following code:

[build]
publish = "public"
command = "npm run deploy"

[context.production.environment]
NODE_VERSION = "17.0.1"

[functions]
directory = "functions/"

With the function files created, let’s start adding our code. Start with the functions/create_comment/create_comment.js file.

const axios = require("axios");

const handler = async (event) => {
    try {
        let payload = JSON.parse(event.body);
        payload.timestamp = new Date();
        const response = await axios({
            "method": "POST",
            "url": `https://data.mongodb-api.com/app/${process.env.MONGODB_DATA_APP_ID}/endpoint/data/beta/action/insertOne`,
            "headers": {
                "api-key": `${process.env.MONGODB_DATA_API_KEY}`
            },
            "data": {
                "dataSource": "<YOUR_CLUSTER_NAME>",
                "database": "netlify",
                "collection": "comments",
                "document": payload
            }
        });
        return {
            statusCode: 200,
            headers: {
                "content-type": "application/json"
            },
            body: JSON.stringify(response.data)
        };
    } catch (error) {
        return { statusCode: 500, body: error.toString() }
    }
}

module.exports = { handler }

Because we’re using the MongoDB Atlas Data API, we’re using HTTP requests in our functions rather than the code we saw for Realm.

The above code will create a timestamp for our comment and then it will do an HTTP request, passing in the payload as well as other information required by the API. Because we’re using an API, we need some keys for security.

From the MongoDB Cloud, click on the “Data API” link and proceed to create an API key. Make note of this API key because you’ll need it in a later step beyond this section. Also make note of the “URL Endpoint” as it will be required for your HTTP requests from the Netlify Function.

In the function code, I’m using environment variables because that’s what you’d use in Netlify. Add the key information and app id information to your system environment variables if you wish to test this locally.

Now we have the next endpoint. Open the project’s functions/get_comments/get_comments.js file and add the following:

const axios = require("axios");

const handler = async (event) => {
    try {
        
        const commentFilter = {};

        if (event.queryStringParameters.hasOwnProperty("url")) {
            commentFilter["url"] = decodeURIComponent(event.queryStringParameters.url);
        }

        if (event.queryStringParameters.hasOwnProperty("last_built")) {
            commentFilter["timestamp"] = {
                "$gt": new Date(decodeURIComponent(event.queryStringParameters.last_built))
            }
        }

        const response = await axios({
            "method": "POST",
            "url": `https://data.mongodb-api.com/app/${process.env.MONGODB_DATA_APP_ID}/endpoint/data/beta/action/find`,
            "headers": {
                "api-key": `${process.env.MONGODB_DATA_API_KEY}`
            },
            "data": {
                "dataSource": "<YOUR_CLUSTER_NAME>",
                "database": "netlify",
                "collection": "comments",
                "filter": commentFilter
            }
        });
        return {
            statusCode: 200,
            headers: {
                "content-type": "application/json"
            },
            body: JSON.stringify(response.data.documents)
        };
    } catch (error) {
        return { statusCode: 500, body: error.toString() }
    }
}

module.exports = { handler }

We’re still obtaining the query parameters from our base.njk file, but we are making an HTTP request to the Data API for the list of comments.

Our functions are done, but we need to make a few modifications to our Eleventy website to do things the Eleventy way instead of Realm.

First open the project’s src/_includes/layouts/base.njk file and update the fetch operators.

fetch(`/.netlify/functions/get_comments?url=${encodedPageUrl}&last_built=${encodedLastBuilt}`)

The above fetch will use the get_comments function instead of a Realm HTTP endpoint. We can do something similar for creating comments:

fetch("/.netlify/functions/create_comment", { /* ... */ })

This works great for on-demand comments at runtime, but we can’t use these local paths during build time. We’ll end up with a bunch of errors.

So how do we get the src/_data/comments.js file working?

Since we can’t make an HTTP request to a Netlify function at build time, we can just interact with the JavaScript directly in Node.js. Remember, we’re talking about the difference between Node.js and client-side JavaScript.

Open the project’s src/_data/comments.js file and modify it to look like the following:

const { AssetCache } = require("@11ty/eleventy-cache-assets");
const axios = require("axios");
const commentPreloader = require("../../functions/get_comments/get_comments").handler;

module.exports = async function () {

    let asset = new AssetCache("comments");

    if (asset.isCacheValid("1d")) {
        return asset.getCachedValue();
    }

    try {
        const response = await commentPreloader({ queryStringParameters: {} }).then(response => JSON.parse(response.body));
        await asset.save(response, "json");
        return response;
    } catch (error) {
        console.error(error);
        return null;
    }

}

You see, we’re importing the get_comments function and calling it directly. The client-side JavaScript won’t have access to this file, but our build time JavaScript does. So now when we build, we’re still obtaining the comments and caching them, but this time, we’re using the Data API.

If you ran netlify dev from the command line, you should be able to locally test these Netlify functions as well as the Eleventy build.

Configuring Netlify for continuous deployment of the Eleventy blog with comments

As of right now, we have two different approaches to comments in a static website, but they’re working semi-locally. We need to get them properly deployed to Netlify.

First up, let’s add a few more scripts to our package.json file:

"scripts": {
    "clean": "rimraf public",
    "serve": "npm run clean; eleventy --serve",
    "build": "npm run clean; eleventy --input src --output public",
    "realm": "cd realm; cross-var realm-cli login --api-key=$REALM_API_KEY --private-api-key=$REALM_PRIVATE_API_KEY; realm-cli push -y",
    "deploy": "npm run realm; npm run build"
},

The realm script will log into Realm using whatever we have in our environment variables and the deploy script will run our realm and our build scripts. Netlify will rely on the deploy script in our pipeline.

Remember, we’re assuming you’re already set up with Netlify and it’s connected to one of your Git accounts.

Create a new site. In the “Site Settings,” find the “Build & Deploy -> Environment” settings.

Netlify Environment Settings

If you’re been using all my environment variable names, this is where you’re going to want to add them. If you haven’t been keeping track of your keys, you may have to generate new ones.

Once the environment variables are in place, you should end up with the same experience as you got when running things locally.

Try pushing your repository to your Git remote. Check out the Netlify deployment logs and see if everything worked out.

Conclusion

You just saw how to add comments to a static website hosted on Netlify. In particular, I chose to use Eleventy, but you should be able to accomplish similar things regardless of the static site generator that you chose.

In this example, we saw two of many possible options, the first being to use Realm Functions and Realm HTTP Endpoints, the other to use Netlify Functions and the MongoDB Atlas Data API. Both work well, and because our example pre-loads all comments at build time, you’re going to end up with a blazing fast site.

Make sure to check out the MongoDB Community Forums to find more use cases and engage with the community.

This content first appeared on MongoDB.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.