Stop delaying. Share knowledge on a blog built with Eleventy.

Have you ever felt the desire to create a website, but not acted upon it? Blogging has been on my mind lately. But, starting is a big hurdle for me. Today, I challenge myself to develop a blog as fast as possible and focus on three points:

Using Lighthouse as a measurement tool, the blog should score 100 on all criteria.

Developer experience
It should be easy for the author to create new posts for the blog.

Progressive enchancement
The blog follows the progressive enhancement philosophy. Basic functionality is available for every user, with the best experience only available for users with a modern browser.


Due to my habit of procrastinating, I challenge myself to start a blog as soon as possible, keeping the points above in mind as well.

For a blog with almost no dynamic data, static site generation is a great option to achieve better performance. A static site generates the pages at build time rather than run time like server-side rendering or client-side rendering. When static generated pages are used, there is no need to fetch data from a database, API, etc. during run time.

To generate a static blog, we will be using Eleventy. Using the blog example from Eleventy as a base for our blog. Clone the repository:

git clone https://github.com/11ty/eleventy-base-blog.git my-blog

Run npm install and edit the metadata in _data/metadata.json.

Integrating Tailwind CSS

As I want to create a blog fast, I'll use Tailwind CSS. Tailwind is a utility-first framework that makes building and designing 10 times faster (at least for me). I don't have to worry about classnames anymore and can easily customize the blog design. Of course, the freedom to customise can also be a disadvantage. In the case of the blog, the impact is zero to none. You can skip this part if you do not want to use Tailwind.

Install packages

First, install the following plugins:
npm install tailwindcss autoprefixer eleventy-plugin-postcss

The framework which we can utilise to style our blog.

Write down CSS without worrying about vendor prefixes like -webkit, -moz etc.

Eleventy plugin postcss
This enables PostCSS support in Eleventy. If you want to do this yourself, you can, and I will probably do it as well. But, for now, I want to get up and running as fast as possible.

I will also use TailwindUI. TailwindUI offers various sample components that have been set up using the framework. This is a source of inspiration and makes it easy to create a layout for the blog.

Make it work

It is now time to get the tooling up and running. Let's start by creating the PostCSS config.

// postcss.config.js

module.exports  =  {
	plugins:  {
	    tailwindcss:  {},
	    autoprefixer:  {},

Now we need to create the Tailwind configuration.

// tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
	content: [
	theme:  {
	    extend:  {},

Specify the files that use Tailwind classes in the content array. The compiler will add these classes to the stylesheet.

You can add Tailwind classes into your nunjucks files and the compiler will load just those classes into the stylesheet. By keeping the stylesheet small and free of unnecessary classes, the browser does not have to download a huge CSS file, and as a result, performs better.

Since we'll be using Tailwind, we can remove the default CSS functionality from the example blog. Remove references to prism-base16-monokai.dark.css and prism-diff.css. Also remove the following line from eleventy.config.js: eleventyConfig.addPassthroughCopy("css");.

Replace the content in index.css with:

/* css/index.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

And add the PostCSS plugin in the eleventy.config.js
const PostCSSPlugin = require("eleventy-plugin-postcss");

Run npm run serve and you can start tinkering with Tailwind in Eleventy.

Integrating dev.to API

Alright, now that Tailwind is done, let's think about creating blog posts. I don’t want to deal with creating an editor and adding an option for comments and storing them in some sort of database.

The process of creating an editor and dealing with comments is something I would rather skip. My ultimate goal is to keep things as simple as possible. This is where dev.to comes in. Through the API, I will be able to post articles on the forum and my blog.

Dev.to consist of an editor and comments. Users can engage with the content on dev.to. I do not have to build everything myself. Also, dev.to is already generating more traffic than the newly set up blog.

If you want to get all the posts from your account into your blog, you will need an API key. Under settings > extensions, you will find the key.

A window where an API key can be generated

Please make sure your API key is safe and secure. Do not publish the key into your repository. We will do this through an environment file. Let's add support for an environment file.

Run npm install dotenv

Create an env.example file with DEVTO_API_KEY=, do not fill in the API key. This file shows which keys exist. Commit this file to your repository.

Afterwards, copy the env.example file to .env and insert the API key. Make sure you include .env in the .gitignore.

Import dotenv in the .eleventy.js file:

After the API key has been incorporated into the code. You can retrieve the articles from your dev.to account without having to publicize the key.

Now we need a way to fetch the articles and actually use them on our pages. Luckily, Eleventy provides an easy way to store API response data. When we look at our project, we will see a folder called data. Create a new file there called posts.js.

// posts.js

const EleventyFetch = require("@11ty/eleventy-fetch");

module.exports = async function() {
  try {
    let url = "https://dev.to/api/articles/me";
	let posts = await EleventyFetch(url, {
		duration: "1d",
		type: "json",
		fetchOptions: {
			headers: {
				"api-key": process.env.DEVTO_API_KEY

	return posts;
	} catch(e) {
		console.log('Failed to fetch articles. Return empty array.');
		return [];

The posts file executes a request with the API key and retrieves all published articles from your account. Since the file name is called posts, the variable to access the articles will also be called posts.

Make sure you use the @11ty/eleventy-fetch package.

npm install @11ty/eleventy-fetch

This package ensures that articles are cached locally and as a result, the dev.to API is not called with every build of the project.


We've installed everything to start building our pages. You can copy mine or you can easily tinker the pages yourself with Tailwind. Let’s create the homepage, articles and single article page. Change the following files:


<body class="min-h-full flex flex-col h-screen">
	<header class="flex justify-between items-center p-4 container mx-auto">
		<nav class="w-2/5">
				{%- for entry in collections.all | eleventyNavigation %}
					<li class="md:inline mb-3 md:mb-0 hover:text-indigo-600 {% if entry.url == page.url %}text-indigo-600 font-bold{% endif %}">
						<a  href="{{ entry.url | url }}" class="py-3 pr-3 md:pl3">{{ entry.title }}</a>
				{%- endfor %}

		<a href="{{ '/' | url }}" class="font-bold text-3xl flex-1 text-center" rel="home">{{ metadata.site_name }}</a>

		<nav class="w-2/5">
			<ul class="text-right">
				<li class="md:inline mb-3 md:mb-0 md:ml-3 hover:text-indigo-600">
					<a href="https://twitter.com/j1sc2s" target="_blank">Twitter</a>
				<li class="md:inline mb-3 md:mb-0 md:ml-3 hover:text-indigo-600">
					<a href="https://github.com/jstnjs" target="_blank">GitHub</a>

	<main{% if templateClass %} class="{{ templateClass }} flex-1"{% endif %}>
		<div class="container mx-auto p-4">
			<div class="max-w-lg mx-auto">
				{{ content  |  safe }}

	<header class="flex items-center">
		<h2 class="font-bold text-2xl my-4">About</h2>

	<p class="text-slate-700">Add your fantastic intro here!</p>

<section class="my-8">
	<header class="flex items-center">
		<h2 class="font-bold text-2xl my-4">Posts</h2>

	{% include "postslist.njk" %}
// _includes/postslist.njk

	{% for post in posts | reverse %}
		<article class="first:mt-0 first:pt-0 mt-6 pt-6 border-t border-gray-300 first:border-none">

			{% if post.cover_image %}
				<img class="mb-4 rounded" width="512" height="215" alt="Cover image for {{post.title}}" src="{{post.cover_image}}"/>
			{% endif %}

			<header class="text-sm text-gray-500">
				<time datetime="{{ post.published_at }}">{{ post.published_at  |  readableDate }}</time>

			<a href="/posts/{{post.slug}}/" class="mt-2 block">
				<p class="text-xl font-semibold text-gray-900">{{post.title}}</p>
				<p class="mt-3 text-base text-gray-500">{{post.description}}</p>

			<footer class="mt-3">
				<a href="/posts/{{post.slug}}/" class="block text-base font-semibold text-indigo-600 hover:text-indigo-500">Read full story</a>
	{% endfor %}

As you can see, because of our posts.js data file we can access the variable posts. If you have published posts on your account, the posts will be shown on the homepage.

Using permalinks, Eleventy allows us to dynamically generate the URL for a single post. The dev.to API provides a property called slug. This is the name of the post transformed to a URL path. Let's use this in our blog so that you do not need to redirect to dev.to every time a user clicks on a preview from an article. Get users to engage with your content by keeping them on your page.

Add the following file named post.njk into the root and add it also into the content array in your tailwind.config.js.

  data: posts
  size: 1
  alias: post
permalink: posts/{{ post.slug }}/
type: article
  title: "{{ post.title }}"
  description: "{{ post.description }}"
layout: layouts/base.njk
templateClass: tmpl-post

<article class="prose prose-slate max-w-none">
	<time datetime="{{ post.published_at }}" class="text-sm text-gray-500">{{ post.published_at | readableDate }}</time>
	<h1>{{ post.title }}</h1>

	{{ post.body_markdown | markdown | safe }}

In this file we use a markdown filter. Remove the markdown library from the .eleventy.js file and add a markdown filter.

const md = new markdownIt({
	html: true,
	breaks: true,
eleventyConfig.addFilter("markdown", (content) => {
	return  md.render(content);

Since the markdown returned from the API is in one property, we cannot style every element individually with Tailwind. Fortunately, Tailwind has created a plugin that resolves this problem. Run the following command and add the plugin to the configuration file.

npm install -D @tailwindcss/typography

// tailwind.config.js

plugins: [

This file also uses a readableDate filter. Replace the filter with the following:

eleventyConfig.addFilter("readableDate", dateObj => {
    if(dateObj) {
        return DateTime.fromISO(dateObj, {zone: 'utc'}).toFormat("dd LLL yyyy");

    return DateTime.now().toFormat("dd LLL yyyy");

Refresh the page. Looks good, doesn't it? The plugin formats the HTML you have no control over, using the prose class.


The performance of the blog is already good due to the static site generation, but it can still be improved. HTML and CSS are not minified yet. Let's start with HTML.

npm install html-minifier

Import the package into the eleventy config
const htmlmin = require("html-minifier");

Add the following transform in the config and adapt it to your needs.

// .eleventy.js

eleventyConfig.addTransform("htmlmin",  function(content,  outputPath) {
	if(outputPath && outputPath.endsWith(".html")) {
		let minified = htmlmin.minify(content, {
			useShortDoctype: true,
			removeComments: true,
			collapseWhitespace: true
		return minified;
	return content;

That’s it! All HTML files in the output are minified.

Next up CSS. This one is a bit different because we use Tailwind with PostCSS. Luckily Tailwind provides a optimization for production page. Let’s start with installing cssnano.

npm install cssnano

Update the config with:

// postcss.config.js

module.exports = {
	plugins: {
		tailwindcss: {},
		autoprefixer: {},
		...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {})

A standard Eleventy build is a production-ready build, but it will not trigger cssnano, because the node environment is not in production. There are several ways to fix this. One of them is to create a production script. In the package.json file add the following script "build:prod": "NODE_ENV=production npx @11ty/eleventy" Whenever you run npm run build:prod, the site will be built with minified CSS.

Alright, the minifying is done. What else? Did you know you can serve HTML, CSS and JS compressed? A lot of websites still use gzip, but there’s also Brotli. Brotli is specifically made for the web and compresses a lot better than gzip in most cases.

Luckily I’m deploying my blog to Netlify, which supports Brotli by default!

Wrapping up

Thank you for reading. I hope you found it useful. The structure of the project could be much better and this is also adjustable with Eleventy.

My blog will remain open source and will be improved over time. In case the project doesn't match the structure of the blog, please check the history of the commits.

The points below are a few gotchas you might run into. If you have any questions, feel free to ask.

Duplicated content

While working on my blog a colleague mentioned that I was creating duplicate content by publishing on dev.to and my blog. This has an impact on SEO. Fortunately, with a link tag you can make Google aware of duplicated content.

<link rel="canonical" href="https://iamjustin.dev/slug-from-article-here"/>

Dev.to support canonical URL. This allows me to keep traffic on dev.to without hurting my blog.

Creating a blog on dev.to that has a canonical URL to your blog

Static dynamic data?

You might notice that the post won't update after deploying. If I make an update on my article on dev.to, it will not be reflected on my blog. There has to be a build step first so that the data is fetched and generated again.

It is possible to avoid this problem by creating a cronjob via a GitHub Action that deploys every night to Netlify.