Display Recent Posts with the DEV API and Netlify Functions
Stephanie Eckles
Posted on April 26, 2020
The method described in this post works for simple HTML sites with no javascript framework, CMS, or static site generator needed, but can be extended to those environments, too.
You can use these steps for any available API, not just DEV, to easily pull live data into your static site hosted on Netlify. In this pure-static HTML version, data will refresh with every page load and not require a build to be triggered when you publish a new post.
The only requirement for this tutorial is a DEV profile with at least one published post, and a Netlify account for hosting.
For reference on what you can accomplish, here's a live demo of a starter I've created with the full code from this tutorial.
Use the template option on the starter repo if you'd just like to grab 'n go to get a site up that displays your DEV posts, just check out the README info.
The following describes how that was set up if you'd like to integrate into an existing project that is hosted on Netlify, and to help with extending the base project.
Step 1: Get a DEV API Key
On DEV, be sure you're logged in and go to Account Settings and generate an API key - keep this tab open for the next step
Step 2: Create Netlify Environment Variable
Login to Netlify and select your site, then within Settings > Build & Deploy > Environment create a new variable assigned to the key DEVTO
with the value being your DEV API key.
Step 3: Create the Netlify Function
Netlify functions are the magic that allows safely querying an API (and many other things) on what is otherwise a truly static site.
First, create a directory where you want to store your Netlify functions. You will define this either in Netlify Settings > Functions > Deploy Settings, or in the netlify.toml
file so that Netlify that they exist so they are processed.
Example netlify.toml
file with functions directory defined:
[build]
# Directory with the serverless Lambda functions to deploy to AWS.
functions = "functions"
For simplicity, create functions/devto.js
at the root of your project.
We will write our function with javascript and use axios to fetch posts from the DEV API.
Axios does not need to be in your local package.json
as Netlify will include it upon processing the function. But we do start by requiring it:
const axios = require("axios");
Then, we create a variable with the base API URL for a user's published posts, defaulting to 9
returned posts:
const apiRoot = "https://dev.to/api/articles/me/published?per_page=9";
Next, we create the primary function handler. This is pretty flexible, the key is that we return what we want to be displayed on our endpoint via the callback
function that is passed into the handler.
Axios is used to get results from the DEV API, and then we map over them to customize what we want to appear in our customized API. We grab the title
, url
, description
, and tags
. We do a join
on the tag_list
to create a simple string for display purposes.
exports.handler = async (event, context, callback) => {
try {
const { data } = await axios.get(apiRoot, { headers: { "api-key": process.env.DEVTO } });
let response = [];
// Grab the items and re-format to the fields we want
if (data.length) {
response = data.map((item) => ({
title: item.title,
url: item.url,
description: item.description,
tags: item.tag_list.join(", "),
}));
}
callback(null, {
statusCode: 200,
body: JSON.stringify(response),
});
} catch (err) {
callback(err);
}
};
Credit to Raymond and Andy whose implementations helped steer me in the right direction
Step 4: Publish the Function
If you do not have branch deploys turned on, you will want to so that you can verify the function and results on a preview deploy. It's a super awesome feature of Netlify, and you can update to use it from Settings > Build & Deploy > Deploy contexts and select an option besides "None". You can certainly revert after making this update.
You can now commit your changes, and then go to the "Deploy" section of your Netlify dashboard. Once the build publishes, you can click on the bolded deploy title to launch the preview URL.
All functions once published are available off the site in the following format:
[preview-url]/.netlify/[functionsdir]/[functionname]
So for this deploy it will be the following if you used the suggested names:
[preview-url]/.netlify/functions/devto
An example payload should look like:
[
{
"title": "CSS-Only Accessible Dropdown Navigation Menu",
"url": "https://dev.to/5t3ph/css-only-accessible-dropdown-navigation-menu-1f95",
"description": "This is the seventh post in a series examining modern CSS solutions to problems I've been solving ov...",
"tags": "css, html, webdev, a11y"
},
// ...and so on
]
Local Testing Data
Due to CORS, you will not be able to fetch your remote endpoint from your local build.
You now have two options: copy the results into a local file to use for testing, or setup the Netlify CLI to build functions locally.
I'm going to proceed with the local data option as it's more beginner-friendly.
So for that, copy the contents of your endpoint into a local file called postdata.json
which you will likely want to exclude from commits with .gitignore
. We will reference this file to help build the next step.
Step 6: Fetch Data From the Netlify Endpoint
Back in your website project, create a new javascript file: posts.js
.
First, we will setup a variable to hold the value of the Netlify endpoint URL, but if we have a window.location.port
value we an assume that's a local build and change to point to our local test data file instead:
let postsApi = "/.netlify/functions/devto";
// Use local test data if not live site
if(window.location.port) {
postsApi = "/js/postdata.json";
}
Next, we will use fetch
to get the results, convert the stringified data to JSON, and then pass it to a custom function that we'll write next:
fetch(postsApi, {
method: "GET",
})
.then((response) => response.json())
.then((data) => {
// Pass to post template and output function
createPostList(data);
})
.catch((error) => {
console.error("Error:", error);
});
Step 7: Define the Posts Placeholder
We need to define a location for the output within an HTML file.
Wherever you want the posts to display, create the following placeholder:
<div class="posts"></div>
The important part is the class which we will use to locate the placeholder. You can update it to a class of your choosing, or an id
if you prefer.
Then, go ahead and add a script tag sourcing posts.js
at the end of the HTML file prior to the closing </body>
tag:
<script src="js/posts.js"></script>
Step 8: Create the Display Function
Back in posts.js
, the first thing we'll do at the top of the file is create a variable to reference our placeholder:
const postList = document.querySelector(".posts");
Then it's time to write the createPostList
function.
Recall that it is being passed the body
of the data we already customized, so we map over each post
, and use destructuring to easily gain access to the value of each piece of post data.
Following that, we define the template using a template literal to place the data an li
and other appropriate HTML elements.
const createPostList = (posts) => {
const items = posts.map((post) => {
const {
title,
url,
description,
tags
} = post;
return `<li class="card">
<div class="card__content">
<a href="${url}" class="card__title">${title}</a>
<p>${description}</p>
<em>${tags}</em>
</div>
</div>
</li>`;
});
const list = `<ul class="card-wrapper" role="list">${items.join("")}</ul>`;
postList.innerHTML = list;
postList.classList.add("loaded");
}
The function ends by joining the li
into ul
, and finally placing the completed list into our placeholder as innerHTML
, and adding a loaded
class for any CSS that you wish to occur once data is present.
Optional: Minimal Card Layout CSS
If you need it, here's the minimal CSS to produce responsive "cards" that use CSS grid to place in auto columns of 1-3 across depending on viewport size (does not include full visual effects from preview example for brevity):
.posts {
// Reduce jarring effect when posts loaded and height adjusts if you have other elements on your page
min-height: 60vh;
transition: 320ms opacity ease-in;
opacity: 0;
}
.posts.loaded {
// Fade in to make appearance more pleasant
opacity: 1;
}
.card-wrapper {
display: grid;
grid-gap: 2em;
grid-template-columns: repeat(auto-fit, minmax(25ch, 1fr));
padding-left: 0;
list-styles: none;
}
.card {
display: flex;
flex-direction: column;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.18);
}
.card__title {
margin-bottom: 1rem;
color: blue;
text-decoration: none;
}
.card__content {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 24px;
}
.card__content p {
line-height: 1.4;
margin: 0 0 1rem;
}
.card__content *:last-child {
margin-top: auto;
}
Step 8: Commit and Preview Deploy
Commit your changes and review once more on the Netlify branch preview to be sure that the deployed version that queries the live Netlify function displays just like the local version without error.
If it does, then all that's left is to merge into your master branch for live site deploy when you're ready! 🚀
API Options and Customization
Review the full DEV API docs >
Change number of returned posts
Open functions/devto.js
and in the $apiRoot
variable change the per_page
value. DEV API allows values up to 1000. You could extend this to handle pagination and retrieve more than that in total.
Change returned values from DEV API
Open functions/devto.js
and in the generated map, add or remove values as desired. Review the DEV API docs for a sample of a returned API object.
Change post template
You can change anything about the markup used in the createPostList
function.
Review previous section if you want to add additional API values to display.
If you need IE11 or under support you may want to run the content of js/posts.js
through the online Babel compiler to produce an alternate to the template literal used to create the post template.
Posted on April 26, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.