Stop delaying. Share knowledge on a blog built with Eleventy.
Tags: performance, tailwindcss, eleventy, nunjucks
Published: 2022-07-19 12:57:28

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:

**Performance**
Using [Lighthouse](https://github.com/GoogleChrome/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](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement). Basic functionality is available for every user, with the best experience only available for users with a modern browser.

## Starting
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](https://www.11ty.dev/). Using the [blog example from Eleventy](https://github.com/11ty/eleventy-base-blog) 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](https://tailwindcss.com). 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`

**[Tailwindcss](https://tailwindcss.com)**
The framework which we can utilise to style our blog.

**[Autoprefixer](https://github.com/postcss/autoprefixer)**
Write down CSS without worrying about vendor prefixes like -webkit, -moz etc.

**[Eleventy plugin postcss](https://www.npmjs.com/package/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](https://tailwindui.com). 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.

```javascript
// postcss.config.js

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

Now we need to create the Tailwind configuration.

```javascript
// tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
	content: [
	    '_includes/**/*.njk',
	],
	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
/* 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");`
`eleventyConfig.addPlugin(PostCSSPlugin);`

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](https://developers.forem.com/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](https://dev.to/settings/extensions). Under settings > extensions, you will find the key.

![A window where an API key can be generated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2iwsq1xohljmi4lblx44.png)

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:
`require('dotenv').config();`

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`.
```javascript
// 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](https://github.com/11ty/eleventy-fetch). 

`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.

## Building
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:

```html
//_includes/layouts/base.njk

<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">
			<ul>
				{%- 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>
					</li>
				{%- endfor %}
			</ul>
		</nav>

		<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>
				<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>
				</li>
			</ul>
		</nav>
	</header>

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

```html
//_includes/layouts/home.njk

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

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

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

	{% include "postslist.njk" %}
</section>
```

```html
// _includes/postslist.njk

<div>
	{% 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>
			</header>

			<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>
			</a>

			<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>
			</footer>
		</article>
	{% endfor %}
</div>
```

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](https://www.11ty.dev/docs/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`.

```html
---
pagination:
  data: posts
  size: 1
  alias: post
permalink: posts/{{ post.slug }}/
type: article
eleventyComputed:
  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 }}
</article>
```


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

```javascript
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`

```javascript
// tailwind.config.js

plugins: [
	require('@tailwindcss/typography'),
],
```

This file also uses a readableDate filter. Replace the filter with the following:
```javascript
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.

## Performance
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.

```javascript
// .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](https://tailwindcss.com/docs/optimizing-for-production) page. Let’s start with installing cssnano.

`npm install cssnano`

Update the config with:

```javascript
// 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](https://www.gzip.org), but there’s also [Brotli]( https://github.com/google/brotli). Brotli is specifically made for the web and [compresses ***a lot*** better than gzip](https://tech.oyorooms.com/how-brotli-compression-gave-us-37-latency-improvement-14d41e50fee4) in most cases.

Luckily I’m deploying my blog to [Netlify](https://www.netlify.com), 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](https://github.com/jstnjs/iamjustin.dev) 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]( https://developers.google.com/search/docs/advanced/crawling/consolidate-duplicate-urls) 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qnh1w904g000iypxvaa4.png)


### 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](https://www.voorhoede.nl/en/blog/scheduling-netlify-deploys-with-github-actions) that deploys every night to Netlify.