I wanted a CMS I could use on my phone. I was playing with my newly created dev.to profile when I saw the Stackbit integration. My site went live in an hour and I felt proud. Then I realized I needed something better than Stackbit because:
- The major selling point was a visual theme editor (not available if your site is generated).
- The choices for SSG did not include 11ty.
- It did not feel easy to optimize the site.
So instead, I built my own integration with DEV and 11ty. If you want to stop reading, I've made it open source. Feel free to install it and add your own API key (and then let me know how it goes).
If you want to see the finished product, head to my blog: https://winstonpuckett.com.
The rest of this blog post explains the exciting bits in how I managed to do such a thing.
Step 1: Grab data from DEV
DEV has an amazing API. The problem was going to be how to get posts into 11ty. Here's where I found the solution. It turns out, you can add API requests as a .js file in the _data folder. Here's what mine looks like:
// from src/_data/devPosts.js
const Cache = require("@11ty/eleventy-cache-assets");
module.exports = async function() {
// notice the endpoint
let devPosts = await Cache('https://dev.to/api/articles/me', {
// cache this for 1 hour.
duration: '1h',
type: 'json',
fetchOptions: {
headers: {
// notice the environment var
'api-key': `${process.env.DEV_API_KEY}`,
}
}
});
return devPosts;
};
Now that your data is in devPosts.js, it can be referenced anywhere from the variable "devPosts". If you want to generate a cards for your posts, you could use the following in a liquid template.
{% comment %} from src/_includes/components/posts.liquid {% endcomment %}
{%- for post in devPosts -%}
<div class="card">
<a href="/posts/{{ post.title | removeNonAlphanumericCharacters | slug }}" ><image class="card__image" loading="lazy" src="{{ post.cover_image }}" alt="image for blog post: {{ post.title }}"></a>
<a class="card__title" href="/posts/{{ post.title | removeNonAlphanumericCharacters | slug }}">{{ post.title }}</a>
<p class="card__description">{{ post.description }}</p>
<p class="card__date">{{ post.published_at | formatDate }}</p>
</div>
{%- endfor -%}
Create pages from data
Luckily, 11ty already has a great tutorial on this. I added the heading to posts.md, and it generated a page for each blog post.
pagination:
data: devPosts
size: 1
alias: post
permalink: "posts/{{ post.title | removeNonAlphanumericCharacters | slug }}/"
Componentizing
I didn't want to copy/paste the style tags between pages. I've also liked the idea of inlining your style tag so it's not a separate http request. 11ty has a way to do all of this!
Once I realized I could inject css, I wondered if I could inject one template into another (yes, instead of using templates. Composition over inheritance, right? Also, react does it...)
I'm not sure if this is something 11ty intended to build into the language, so be really cautious if you're going to do this yourself. Also, it works for liquid templates, but not nunjucks. Let me know if you find any other template-languages it works with.
Simply create a .liquid file in your _includes folder and insert some content like so:
{% comment %} from src/_includes/components/header.liquid {% endcomment %}
{% comment %} notice we're injecting additional content on the line below {% endcomment %}
{% capture headerCss %}{% include css/header.css %}{% endcapture %}
<style>{{ headerCss | cssmin | safe }}</style>
<header>
<nav>
<div class="nav__bar">
<a class="nav__homebutton" href="/">
<span class="nav__logo"><img loading="lazy" src="{{ devProfile.profile_image }}" class="avatar"
alt="Author Avatar"></span>
<div class="nav__titlewrapper">
<span class="nav__title">{{ devProfile.name }}</span>
<span class="nav__subtitle">{{ devProfile.summary }}</span>
</div>
</a>
<button class="nav__hamburger" aria-label="Home button" onclick="const links = document.querySelector('.nav__links'); links.classList.contains('active') ? links.classList.remove('active') : links.classList.add('active');const hamburger = document.querySelector('.nav__hamburger'); hamburger.classList.contains('active') ? hamburger.classList.remove('active') : hamburger.classList.add('active');"><svg
width="2em" height="2em" viewBox="0 0 16 16" class="bi bi-list" fill="currentColor"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd"
d="M2.5 11.5A.5.5 0 0 1 3 11h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4A.5.5 0 0 1 3 3h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z" />
</svg></button>
</div>
<ul class="nav__links">
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
</nav>
</header>
Now that we have a file, Let's inject that template in.
{% comment %} from src/index.liquid {% endcomment %}
{% capture header %}{% include components/header.liquid %}{% endcapture %}
{{ header }}
The result injects the header from header.liquid and then processes it as if it was part of the original .liquid file! Or it preprocesses the header.liquid file and injects it into the index.liquid file... Either way, we've just injected a lightweight, variable-less component!
Parameterizing for Open Source
At this point, my DEV API key was still in the solution. I didn't want that ever checked into source control, so I needed a way to make the API key part of the environment. It took a long time for me to learn this for some reason, but it's set up for you and ready to go. Node.js (what 11ty is built on) exposes a "process.env" variable where you can use dot syntax to access any environment variable. I've added an env.js file to allow templates to use environment variables too. This is important for the next section.
// from src/_data/env.js
// This file is a comprehensive list of all environment variables required to run
// the project. The "env" data source can be used in templates, but be aware that
// other data files might use process.env directly. For instance, devPosts.js.
module.exports = {
DEV_API_KEY: process.env.DEV_API_KEY,
GOOGLE_ANALYTICS_TRACKING_ID: process.env.GOOGLE_ANALYTICS_TRACKING_ID,
GOOGLE_ADSENSE_DATA_AD_CLIENT: process.env.GOOGLE_ADSENSE_DATA_AD_CLIENT
};
Adding Google Analytics / Google Adsense
One of the points of having my blog is monetization. I didn't want to have the tags in my local environment, but I did want to add both of these for production. I also wanted to give others the chance to add their own analytics / adsense accounts. So I decided to make these optional environment variables. Then I added "components" for both of these and inject the analytics on every page and the adsense on every blog page.
{% comment %} from src/components/googleAnalytics.liquid {% endcomment %}
{% if env.GOOGLE_ANALYTICS_TRACKING_ID and env.GOOGLE_ANALYTICS_TRACKING_ID != "" %}
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id={{ env.GOOGLE_ANALYTICS_TRACKING_ID }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ env.GOOGLE_ANALYTICS_TRACKING_ID }}');
</script>
{% endif %}
Deployment
I'm currently using Netlify for this. There are lots of posts on how to set up a site, so I'll skip to the integration between Netlify and DEV.
I had to make a POST to https://dev.to/api/webhooks/ with the body:
{
"webhook_endpoint": {
"target_url": "<Your target URL from Netlify Build Hooks>",
"source": "DEV",
"events": [
"article_created",
"article_updated",
"article_destroyed"
]
}
}
And the webhook was successfully created. Now every time I create, update, or delete a public article, it sends a request to Netlify and my site is built and published.
Now what?
Go check out my open source repo and the finished product
Top comments (2)
I see that in posts.njk you're using a
title
property inside of thepagination
object with a computed value. I didn't think that possible other than usingeleventyComputed
. Very interesting! Very nice project, thanks for sharing! 😊You're welcome! I didn't know about eleventyComputed. I'll have to look that up!
There were definitely some weird things I had to do to get it to work lol