Tag: Eleventy

Creating Your Own Bragdoc With Eleventy

No matter what stage you’re at as a developer, the tasks we complete—whether big or small—make a huge impact in our personal and professional growth. Unfortunately, those tasks aren’t always recognized because they can easily get lost in the sea of other things that need to get done.

The unnoticed tasks we do fall under what is known as “invisible work,” a concept I stumbled across from a talk titled “Getting Credit for Invisible Work” by Ryan T. Harter. This type of work seeps into the cracks because our brains are not wired to remember things. Yet come review time, we find ourselves repeatedly stuck when trying to recall what we did over the past 6 or 12 months.

To solve this long-established problem, Julia Evans wrote an article suggesting that we keep our own “brag document.” A brag document is exactly what it sounds like. It’s a document where you give yourself permission to brag about all the valuable work you did. Whether it be:

  • How you contributed to a project
  • Helping others
  • Improving existing processes
  • Giving talks or running workshops
  • What you learned
  • Extra-curricular activities (e.g. blogging, talks, personal projects)
  • Awards and career progression

There is no one way to write a brag document, but that didn’t stop Jonny Burch and the team at Progression from building bragdocs.com.

Using their site to build one is a great idea, but what better way to brag about your work than to create your own brag document from scratch?

Today I want to show you how I re-created bragdocs.com using the static site generator Eleventy. With a little bit of JavaScript and CSS, you can get your own up and running!

What are we going to build?

Below is the end result of following this tutorial. You can find the live demo here. It imitates bragdocs.com as a starting point for you to create one from scratch and make it your own.


  • Installing packages in Node.js (version 10 or higher)
  • General understanding of HTML and CSS
  • Markdown, Nunjucks templating, and JavaScript (all are optional, but helpful)
  • Basic programming concepts, including if statements, loops, and accessing variables in JSON

What is Eleventy?

Eleventy is a static site generator. This means that rather than building a full-stack website (front-end and back-end), you have flexibility to write content in any of the following templating languages accepted by Eleventy: HTML, Markdown, Liquid, Nunjucks, Mustache, etc. The content is then processed (using custom templates if you like) to generate static HTML pages, ready for hosting as a fully functioning site.

Setting up our “Hello, World!” Eleventy project

In this tutorial, the repository I’ll be referring to is eleventy-bragdoc, and the final product we’re working towards will be referred to as a “bragdoc.”

With a GitHub repository created with a README.md and .gitignore file for Node, I started setting up an Eleventy project.

Creating a new project

Inside eleventy-bragdoc, I began with the following files:

eleventy-bragdoc ├── README.md └── .gitignore // .gitignore for node

With the terminal navigated inside of eleventy-bragdoc, I initialized the project by running the following command:

npm init -y

This created a package.json file for my node packages.

eleventy-bragdoc ├── package.json // new file ├── README.md └── .gitignore

Next, I installed Eleventy.

npm install @11ty/eleventy

This gave me the following list of files and folders:

eleventy-bragdoc ├── node_modules  // new folder ├── package.json ├── package-lock.json  // new file ├── README.md └── .gitignore

Configuring the Eleventy project

With Eleventy installed, I updated the scripts in the package.json file to include the following commands:

  • The start command serves the project during development which runs Browsersync for hot reload.
  • The build command creates production ready HTML files so that it can be hosted onto a server.
{   // ...   "scripts": {     "start": "eleventy --serve",     "build": "eleventy"   },  //  ... }

Next, I created the required configuration file called .eleventy.js to specify the custom input and output directories.

eleventy-bragdoc ├── .eleventy.js  // new file ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Inside .eleventy.js, I told Eleventy that it’s going to reference what’s inside of the src folder to build the HTML files. The output is then stored inside a folder called public:

module.exports = function(eleventyConfig) {   return {     dir: {       input: "src",       output: "public"     }   } }

Creating front-facing content

To make my first page, I created the src folder that I declared as the input directory in .eleventy.js . Inside it, I added my first page, a Markdown file called index.md

Eleventy works with many templating languages that you can mix and match: HTML, Markdown, Liquid, Nunjucks, JavaScript, Handlebars, Mustache, EJS, Haml, Pug.

eleventy-bragdoc ├── src │   └── index.md  // new file ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

In Eleventy, any key value pairs written between the dashes (---) above and below is considered front matter.

In index.md , I included a title property with the value “11ty x Bragdocs” and some test content underneath the front matter.

--- title: "11ty x Bragdocs" ---  This is the home page.

Building templates

Next, I created a folder which Eleventy expects, called _includes inside of src. This is where the templates, or what Eleventy refers to as layouts, must live. Within that folder, I created a subfolder called layouts for my first template, base.njk

The .njk filetype refers to the templating language Nunjucks.

eleventy-bragdoc ├── src │   ├── _includes  // new folder │   │   └── layouts  // new folder │   │       └── base.njk  // new file │   └── index.md ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

I added an HTML5 boilerplate inside base.njk:

<!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>Document</title> </head> <body>      </body> </html>

Creating pages with templates and front matter

In base.njk , between the <title> tags, I wanted to pull in the title property defined in the front matter of index.md, so I used double curly braces, i.e. {Creating Your Own Bragdoc With Eleventy}, to access this variable. Similarly, in the body, I added <h1> tags and set it with the same title property.

Next, I brought in the rest of the body content from index.md using the content property. Using the provided safe filter, I told Eleventy to render instead of escape any HTML that lives inside the content of the Markdown file.

<!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>{{ title }}</title> </head> <body>   <h1>{{ title }}</h1>   {{ content | safe }} </body> </html>

I then jumped back to index.md and added a layout property to the front matter and referenced base.njk

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  This is the home page.

To give you an idea of what happens when we run the build, the template specified in the layout front matter property is used to wrap the Markdown content. In this example, the compiled HTML will look like what is shown below:

<!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>11ty x Bragdocs</title> </head> <body>   <h1>11ty x Bragdocs</h1>   <p>This is the home page.</p> </body> </html>

Connecting CSS and image folders in build

While this part might not be necessary for all Eleventy projects, CSS and self-hosted images are always good features to add. So, I created two folders in the src directory: css and images.

eleventy-bragdoc ├── src │   ├── css  // new folder │   ├── images  // new folder │   ├── _includes │   │   └── layouts │   │       └── base.njk │   └── index.md ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Then, in .eleventy.js, since I wanted the content inside these folders to be accessible when hosted, I referenced these folders by adding the following configurations:

  • addWatchTarget tells Eleventy that it should recompile when we make a change to a file in this directory (e.g. styles.css in the css folder).
  • addPassthroughCopy tells Eleventy that once the files are compiled, to take the contents of the directory and pass it through to the public directory.

You can read more about how passthrough file copy works in the documentation.

Since I was using the Nunjucks templating system, I added the markdownTemplateEngine property and set it to njk to make sure that it knows to go through Nunjucks first before anything else.

module.exports = function(eleventyConfig) {   eleventyConfig.addWatchTarget("./src/css/")   eleventyConfig.addWatchTarget("./src/images/")   eleventyConfig.addPassthroughCopy("./src/css/")   eleventyConfig.addPassthroughCopy("./src/images/")    return {     dir: {       input: "src",       output: "public"     },     markdownTemplateEngine: "njk"   } }

Then I created a styles.css file in the css folder and gave it something to test with to make sure it worked.

* {   color: teal; }

Since I already configured the css and images folders in .eleventy.js, I was able to reference these files using Eleventy’s URL filter.

To access these self-hosted files I used Eleventy’s URL filters in the href and src property of the css and image tags, respectively.

<!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>{{ title }}</title>    <link rel="stylesheet" href="{{ '/css/styles.css' | url }}">  </head> <body>   <h1>{{ title }}</h1>    <img src="{{ '/images/test_image.jpg' | url }}">    {{ content | safe }} </body> </html>

Now I was ready to serve my Eleventy project.

Serving Eleventy in development

Since I had already defined the custom development scripts in package.json, I was able to run the following command:

npm start

This compiled index.md in the src directory and generated a HTML file in the public folder. Additionally, it launched a hot reload server through Browsersync where I could see the result at http://localhost:8080/

The result so far

With Eleventy running in development, I could start building the rest of the bragdoc.

Building the bragdoc system

With a base Eleventy project in a folder structure similar to what’s shown below, I began building out my bragdoc.

eleventy-bragdoc ├── src │   ├── css │   │   └── styles.css │   ├── images │   │   └── test_image.jpg │   ├── _includes │   │   └── layouts │   │       └── base.njk │   └── index.md ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Creating a collection for bragdoc entries

Eleventy has the ability to create collections that group similar content together. Therefore, I created a folder called posts for my bragdoc entries. Inside that folder, I created multiple Markdown files to represent each entry.

The filenames post-1.md, post-2.md, post-3.md don’t affect anything that is rendered on the webpage

eleventy-bragdoc ├── src │   ├── posts │   │   ├── post-1.md  // new file │   │   ├── post-2.md  // new file │   │   └── post-3.md  // new file │   ├── css │   │   └── styles.css │   ├── images │   │   └── test_image.jpg │   ├── _includes │   │   └── layouts │   │       └── base.njk │   └── index.md ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

The custom properties that I thought would be useful to include:

  • Title
  • Date (by default, posts are sorted chronologically)
  • Categories (a list of values to organize entries)
  • Public / Private (a boolean value—true or false—to determine whether you want to show it on the bragdoc )
  • Icon (a Notion-inspired design element to visually organize entries)

I decided that the description for each entry would be the body content of the Markdown file, as this would give me freedom to add paragraphs, images, code blocks, etc. Additionally, I was not limited to Markdown elements as I could also include HTML and style it using CSS.

Below is an example of a bragdoc entry in a Markdown file:

--- title: Build my own Bragdoc using Eleventy date: 2021-09-19 categories:   - Learning   - Eleventy public: True icon: 🎈 ---  I learned how to use Eleventy to build my own bragdoc!

Some things to note:

  • Links written in Markdown by default do not open in a new blank window. So after some research, I stumbled upon a snippet by Mark Thomas Miller, which I added just before the closing <body> tag in base.njk. This might not be your thing (it’s definitely not Chris’ thing) but just in case you need it:
<script> // Making all external links open in new tabs // Snippet by Mark Thomas Miller  (function () {   const links = document.querySelectorAll("a[href^='https://'], a[href^='http://']")   const host = window.location.hostname    const isInternalLink = link => new URL(link).hostname === host    links.forEach(link => {     if (isInternalLink(link)) return      link.setAttribute("target", "_blank")     link.setAttribute("rel", "noopener")   }) })() </script>
  • The date front matter property must be written in YYYY-MM-DD format.
  • You can assign as many custom front matter properties as you’d like. Just make sure that if you plan on accessing the property in the template, that the property exists in all of the Markdown files using the same template; otherwise it may break the build.
  • Lists in front matter can be written in multiple ways (e.g. an array or single line).

Assigning front matter properties to a collection

Instead of repeatedly assigning front matter properties with the same value in each Markdown file, I created a data directory JSON file to assign the same key-value pair only once across a collection.

To create a data directory file, it must have the same name as the collection, i.e. posts.json. Additionally, the file must also be placed inside the collection folder, i.e. the posts folder.

eleventy-bragdoc ├── src │   ├── posts │   │   ├── posts.json  // new file │   │   ├── post-1.md │   │   ├── post-2.md │   │   └── post-3.md │   ├── css │   │   └── styles.css │   ├── images │   │   └── test_image.jpg │   ├── _includes │   │   └── layouts │   │       └── base.njk │   └── index.md ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

At this point, the posts for the bragdoc had not been defined as a collection yet. To do this, I added the tags property in posts.json. Here I assigned the value “posts” to that property so that I could access the collection by calling collections.posts

And since I didn’t need each post to have its own page, i.e. http://localhost:8080/posts/post-1/, I switched off it’s auto-generated permalink.

{   "tags": "posts",   "permalink": false }

Listing bragdoc entries

Simply put, the bragdoc is a page made up of the entries in the posts collection. To access the front matter properties and body content of the Markdown files, the entries are looped through via Nunjucks.

To do this, I went back to index.md and changed the filetype from Markdown to Nunjucks, i.e. index.njk

eleventy-bragdoc ├── src │   ├── posts │   │   ├── posts.json │   │   ├── post-1.md │   │   ├── post-2.md │   │   └── post-3.md │   ├── css │   │   └── styles.css │   ├── images │   │   └── test_image.jpg │   ├── _includes │   │   └── layouts │   │       └── base.njk │   └── index.njk  // changed filetype ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Next, I replaced the content of index.njk with a Nunjucks for loop.

A Nunjucks function (for loop, if statement, etc.) must include start and end tags.

Since the order of posts by default was in chronological order (oldest first), I added the reverse filter to show the most recent at the top.

To access front matter and render it in HTML (such as the date and title of a post), I had to go through another “data” layer. Accessing properties in front matter requires double curly braces.

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  {% for post in collections.posts | reverse %}   <p>     {{ post.data.date }} - {{ post.data.title }}   </p> {% endfor %}
A little more progress

Filtering bragdoc entries

To filter certain entries, I used the front matter data to check if the public property was set to True. If the property was set to False, the entry did not appear in the bragdoc.

Similarly, when accessing front matter properties, such as public through a Nunjucks function, I again needed to go through another “data” layer.

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  {% for post in collections.posts | reverse %}   {% if post.data.public %}     <p>       {{ post.data.date }} - {{ post.data.title }}     </p>   {% endif %} {% endfor %}
The posts are ordered with the title.

Adding custom data filters

By default, the date property renders something that we’re generally unfamiliar with. So, after some research, I found a custom filter written by Phil Hawksworth. To use the filter, I created a file called dates.js and placed it in a new folder called _filters

eleventy-bragdoc ├── src │   ├── _filters  // new folder │   │   └── dates.js  // new file │   ├── posts │   │   ├── posts.json │   │   ├── post-1.md │   │   ├── post-2.md │   │   └── post-3.md │   ├── css │   │   └── styles.css │   ├── images │   │   └── test_image.jpg │   ├── _includes │   │   └── layouts │   │       └── base.njk │   └── index.njk ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Then, inside dates.js, I added the following:

/* A date formatter filter for Nunjucks  Written by Phil Hawksworth */ module.exports = function(date, part) {   var d = new Date(date);   if(part == 'year') {     return d.getUTCFullYear();   }   var month = [     "January",     "February",     "March",     "April",     "May",     "June",     "July",     "August",     "September",     "October",     "November",     "December"   ];   var ordinal = {     1 : "st",     2 : "nd",     3 : "rd",     21 : "st",     22 : "nd",     23 : "rd",     31 : "st"   };   return month[d.getMonth()] + " " + d.getDate() + (ordinal[d.getDate()] || "th") + " " +d.getUTCFullYear(); }

To access the date filter in the project, I added a new filter in .eleventy.js where I can call it using the custom name dateDisplay

module.exports = function (eleventyConfig) {    // Add filter   eleventyConfig.addFilter("dateDisplay", require("./src/_filters/dates.js") );      eleventyConfig.addPassthroughCopy("./src/css/")   eleventyConfig.addPassthroughCopy("./src/images/")   eleventyConfig.addWatchTarget("./src/css/")   eleventyConfig.addWatchTarget("./src/images/")    return {     dir: {       input: "src",       output: "public"     },     markdownTemplateEngine: "njk"   } }

In index.njk, I assigned the dateDisplay filter to the date variable, rendering it in a human-readable format.

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  {% for post in collections.posts | reverse %}   {% if post.data.public %}     <p>       {{ post.data.date | dateDisplay }} - {{ post.data.title }}     </p>   {% endif %} {% endfor %}

The server needs to be restarted every time you change something in the configuration file.

The posts with updated date formatting.

To return the body content of a post, I called templateContent and added the safe filter so that it rendered any HTML in the Markdown file rather than escaping it.

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  {% for post in collections.posts | reverse %}   {% if post.data.public %}     <p>       {{ post.data.date | dateDisplay }} - {{ post.data.title }}        <br/>       {{ post.templateContent | safe }}     </p>     <br/>   {% endif %} {% endfor %}
The posts with body content.

Finally, I included another for loop to list the values in the categories front matter property.

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  {% for post in collections.posts | reverse %}   {% if post.data.public %}     <p>       {{ post.data.date | dateDisplay }} - {{ post.data.title }}       <br/>       {{ post.templateContent | safe }}       {% for category in post.data.categories %}         <span># {{category}}</span>       {% endfor %}     </p>     <br/>   {% endif %} {% endfor %}

Having finished extracting data from the posts collection, it was time to build out the HTML structure.

Structuring the bragdoc

Partials in Eleventy allow us to repeatably use bits of HTML or templating. This also simplifies the code from one massive template file to manageable pieces that fit together.

Inside the <body> tags of base.njk , I removed everything except the content and snippet.

<!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0">   <title>{{ title }}</title>    <link rel="stylesheet" href="{{ '/css/styles.css' | url }}"> </head> <body>   {{ content | safe }}   <script>     (function () {       const links = document.querySelectorAll("a[href^='https://'], a[href^='http://']")       const host = window.location.hostname        const isInternalLink = link => new URL(link).hostname === host        links.forEach(link => {         if (isInternalLink(link)) return          link.setAttribute("target", "_blank")         link.setAttribute("rel", "noopener")       })     })()   </script> </body> </html>

Next, I created bragdoc-entry.njk which lives inside a new folder called partials

eleventy-bragdoc ├── src │   ├── _filters │   │   └── dates.js │   ├── posts │   │   ├── posts.json │   │   ├── post-1.md │   │   ├── post-2.md │   │   └── post-3.md │   ├── css │   │   └── styles.css │   ├── images │   │   └── test_image.jpg │   ├── _includes │   │   ├── partials  // new folder │   │   │   └── bragdoc-entry.njk  // new file │   │   └── layouts │   │       └── base.njk │   └── index.njk ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Inside bragdoc-entry.njk, I brought over the content that make up the bragdoc entry, written in index.njk. Notice that it doesn’t require any front matter since it is treated as a snippet.

Partials do not extend a template, so they do not need any front matter.

<p>   {{ post.data.date | dateDisplay }} - {{ post.data.title }}   <br/>   {{ post.templateContent | safe }}   {% for category in post.data.categories %}       <span># {{category}}</span>   {% endfor %} </p> <br/>

Then, between the if statement in index.njk, I added an include tag that references the bragdoc-entry.njk partial. By doing this, the content inside bragdoc-entry.njk is repeatably added until the for loop finishes.

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  {% for post in collections.posts | reverse %}   {% if post.data.public %}     {% include 'partials/bragdoc-entry.njk' %}   {% endif %} {% endfor %}

Next, I wrapped the entire for loop with some custom HTML, including a header, profile container and footer. At this point, I also included a profile picture in the images folder and referenced it in the custom HTML using Eleventy’s URL filter.

--- title: "11ty x Bragdocs" layout: "layouts/base.njk" ---  <div class="bragdoc__section" id="bragdoc__section"> <h1 class="bragdoc__header">{{ title }}</h1> <div class="bragdoc__container">   <div class="bragdoc__profile">     <img class="bragdoc__photo" src="{{ '/images/profile_picture.jpg' | url }}">     <h1 class="bragdoc__name">Emily Y Leung</h1>     <div class="role">Computational Designer</div>   </div>   {% for post in collections.posts | reverse %}     {% if post.data.public -%}       {% include 'partials/bragdoc-entry.njk' %}     {% endif %}   {% endfor %}   </div>   <footer>     <div><a target="_blank" href="https://www.bragdocs.com/">Bragdocs</a> inspired theme built with <a target="_blank" href="https://www.11ty.dev/">11ty</a></div>     <div>Made with ♥ by <a target="_blank" href="https://emilyyleung.github.io/">Emily Y Leung</a></div>   </footer> </div>

Then, inside bragdoc-entry.njk, I updated the HTML structure and included classes for styling:

<div class="bragdoc__entry">   <div class="bragdoc__entry-milestone"></div>   <div class="bragdoc__entry-block">     <span class="bragdoc__entry-date">       {{ post.data.date | dateDisplay }}     </span>     <br/>     <h2 class="bragdoc__entry-title"><span class="bragdoc__icon">{{ post.data.icon }}</span> {{ post.data.title }}</h2>     <div class="bragdoc__entry-content">         {{ post.templateContent | safe }}     </div>   </div>   <div class="bragdoc__taglist">   {% for category in post.data.categories %}     <span># {{category}}</span>   {% endfor %}   </div> </div>

Accessing global data

A good way to understand global data is to imagine building a HTML template that someone could use as a base for their website. Rather than searching for specific HTML tags to replace the text, they only need to replace certain values in an external file which then updates the content. This is one of the many things a global data file can do for us.

Eleventy can access global data files written in JSON when they are placed in a folder called _data. So, I created a data.json file that is accessible when I call {{data}} and then pick out whatever properties I had provided in the JSON object.

eleventy-bragdoc ├── src │   ├── _data  // new folder │   │   └── data.json  // new file │   ├── _filters │   │   └── dates.js │   ├── posts │   │   ├── posts.json │   │   ├── post-1.md │   │   ├── post-2.md │   │   └── post-3.md │   ├── css │   │   └── styles.css │   ├── images │   │   ├── profile_picture.jpg │   │   └── test_image.jpg │   ├── _includes │   │   ├── partials │   │   │   └── bragdoc-entry.njk │   │   └── layouts │   │       └── base.njk │   └── index.njk ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Inside data.json, I included properties that were reused throughout the project:

{   "mywebsite": "https://emilyyleung.github.io/",   "myname": "Emily Y Leung",   "myrole": "Computational Designer" }

One great use case was to replace the content in the profile and footer in index.njk

<!-- Profile --> <div class="bragdoc__profile">   <img class="bragdoc__photo" src="{{ '/images/profile_picture.jpg' | url }}">   <h1 class="bragdoc__name">{{ data.myname }}</h1>   <div class="role">{{ data.myrole }}</div> </div> <!-- Footer --> <footer>   <div><a target="_blank" href="https://www.bragdocs.com/">Bragdocs</a> inspired theme built with <a target="_blank" href="https://www.11ty.dev/">11ty</a></div>   <div>Made with ♥ by <a target="_blank" href="{{ data.mywebsite }}">{{ data.myname }}</a></div> </footer>

Styling the bragdoc

With the bragdoc structure completed, I updated the styling in styles.css

To imitate bragdocs.com, I selected some of their colors and stored them in a root variable.

Additionally, I wanted to create multiple themes, so I added a custom data-theme property on top of the :root variable. In this case, the default color theme is “light” regardless of whether data-theme is assigned to the <html> tag. But that also means that if I wanted to create a “dark” theme, I could create a new selector html[data-theme="dark"] in my CSS, and assign alternative colors to the same variables as specified in :root

:root, html[data-theme="light"] {   --logo: black;   --name: black;   --entry-title: black;   --date: #BDBDBD;   --text: #676a6c;   --entry-line: #f1f1f1;   --entry-circle: #ddd;   --background: white;   --text-code: grey;   --code-block: rgba(0,0,0,0.05);   --link-text: #676a6c;   --link-hover: orange;   --quote-block-edge: rgba(255, 165, 0, 0.5);   --quote-block-text: #676a6c;   --table-border: #676a6c;   --footer: #BDBDBD;   --tag: #BDBDBD; }

To reference root variables, call var() where the argument is the name of the property.

Here is an example of how we can use root variables to style the color of text in a <p> tag:

:root {   --text: teal; }  p {   color: var(--text) }

For fun, I added a dark version inspired by Google Material.

html[data-theme="dark"] {   --logo: #FFF;   --name: #FFF;   --entry-title: #dedede;   --date: rgba(255,255,255,0.3);   --text: #999999;   --entry-line: rgba(255,255,255,0.2);   --entry-circle: rgba(255,255,255,0.3);   --background: #121212;   --code-text: rgba(255,255,255,0.5);   --code-block: rgba(255,255,255,0.1);   --link-text: rgba(255,255,255,0.5);   --link-hover: orange;   --quote-block-edge: rgb(255, 165, 0);   --quote-block-text: rgba(255, 165, 0,0.5);   --table-border: #999999;   --footer: rgba(255,255,255,0.3);   --tag: rgba(255,255,255,0.3); }

To control what theme you want to use, add the data-theme property to the <html> tag in base.njk. From there, assign the value associated to the corresponding CSS selector, i.e. “light” or “dark.”

<!DOCTYPE html> <html lang="en" data-theme="light">

Next, I added styling to the <body>, <footer>, bragdoc section, and logo.

body {   font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;   font-size: 13px;   color: var(--text);   background-color: var(--background);   margin: 0;   height: 100vh; }  footer {   margin: 0 auto;   max-width: 500px;   padding-bottom: 1.5em;   text-align: center;   color: var(--footer);   padding-top: 2em;   margin-top: 2em; }  /* Bragdoc Logo */  .bragdoc__header {   margin: 0;   padding: 1em;   font-size: 1.5em;   color: var(--logo) }  /* Bragdoc Body */  .bragdoc__section {   height: 100%;   display: grid;   grid-template-rows: auto 1fr auto;   margin: 0;   padding: 0; }

At this point, the custom tags and classes in the HTML made it simple to replicate the bragdoc layout.

/* Bragdoc User Profile */  .bragdoc__profile {   padding-top: 3em;   padding-bottom: 2em; }  .bragdoc__photo {   width: 8em;   border-radius: 100%;   padding: 0;   height: 8em;   object-fit: cover; }  .bragdoc__name {   color: var(--name);   margin-bottom: 0.25em; }  .bragdoc__icon {   font-family: "Segoe UI Emoji", Times, serif; }  .bragdoc__container {   max-width: 800px;   margin: 0 0 0 30em;   height: 100%; }  .bragdoc__profile-role {   margin: 0; }

Next, I styled the entries to replicate the bragdocs.com timeline design.

/* Individual Bragdoc Entry Blocks */  .bragdoc__entry {   position: relative; }  .bragdoc__entry:first-child {   margin-top: 0; }  .bragdoc__entry:before {   height: 100%;   position: absolute;   background-color: var(--entry-line);   width: 2px;   content: "";   top: 30px; }  .bragdoc__entry:last-child:before {   background-color: var(--background); }  .bragdoc__taglist {   margin-left: 1em;   padding: 1em; }  .bragdoc__taglist > * {   border: 1px solid var(--tag);   padding: 0.25em 0.5em 0.25em 0.5em;   border-radius: 0.5em;   margin-right: 1em; }  /* Entry Content */  .bragdoc__entry-block {   margin-left: 1em;   padding: 1em; }  .bragdoc__entry-title {   margin-top: 4px;   color: var(--entry-title);   font-size: 1.5em; }  .bragdoc__entry-date {   line-height: 3em;   color: var(--date); }  /* Bragdoc milestone circle */  .bragdoc__entry-milestone {   position: absolute;   height: 5px;   width: 5px;   border: 2px solid var(--entry-circle);   background-color: var(--background);   left: 0;   top: 30px;   margin-top: -2px;   margin-left: -3px;   border-radius: 100px; }  /* Bragdoc Entry Content */  .bragdoc__entry-content > * {   margin-bottom: 0.5em;   margin-left: 0; }  .bragdoc__entry-content > h1 {   font-size: 1.15em; }  .bragdoc__entry-content > h2, h3, h4, h5, h6 {   font-size: 1em;   color: var(--text); }

Using CSS media queries, I could also control the size of text as well as the positioning of HTML elements. This makes it work well when viewed on mobile.

/* Make it responsive */  @media only screen and (max-width: 1400px) {    .bragdoc__container {     /* Center the bragdoc*/     margin: 0 auto;   }    .bragdoc__entry-title {     font-size: 1.25em;   } }  @media only screen and (max-width: 870px) {    .bragdoc__container {     padding-left: 2em;     padding-right: 2em;   }    .bragdoc__entry-title {     font-size: 1.15em;   } }

The final touches to the design needed to account for the description (i.e. the Markdown body content) in each entry, which you can find in this Gist.

Given that the CSS has been structured with reference to root variables, we can continue to create more themes. Have a crack at exploring color palettes from Color Hunt or Cooolers.

Deploying the bragdoc to GitHub Pages

Building a project from scratch is fantastic, but sharing it with the world is even better!

While there are a myriad of ways to host a bragdoc, I decided to host it on GitHub Pages. This meant I could use the base URL of my GitHub account and add /eleventy-bragdoc/ to the end of it.

At this point, I had been working from the eleventy-bragdoc repository and had already created a gh-pages branch.

Follow this tutorial for information on how to set up GitHub Pages for your repository.

Configuring the URL path

To configure the URL path for deployment, I included a pathPrefix in .eleventy.js to define the route relative to the base URL.

Without specifying a pathPrefix, the value by default is /, which links to the base URL, i.e. https://emilyyleung.github.io/

Since I already had content on the base URL, I wanted to host it on a sub-page, i.e. https://emilyyleung.github.io/eleventy-bragdoc/

To set the pathPrefix for sub-pages, it must start and end with a slash:

module.exports = function (eleventyConfig) {   // ...   return {     dir: {       input: "src",       output: "public"     },     markdownTemplateEngine: "njk",     pathPrefix: "/eleventy-bragdoc/"   } }

Adding the GitHub Pages dependency

After configuration, I installed GitHub Pages using the terminal:

npm install gh-pages --save-dev

This automatically adds the dependency to package.json

{   // ...     "devDependencies": {     "gh-pages": "^3.2.3"   },   // ... }

Adding a custom terminal script

To deploy the public folder, I added a deploy script and referenced the public folder:

{   // ...   "scripts": {     "start": "eleventy --serve",     "build": "eleventy",     "deploy": "gh-pages -d public"   }   // ... }

Running the build

Just like in development, I navigated my terminal to the eleventy-bragdoc folder. But this time, I ran the following command to rebuild the files into the public folder:

npm run-script build

Then, to deploy to GitHub Pages, I ran the following command:

npm run deploy

Granting access to deploy

At this point, the terminal may ask you to log in via the terminal or through the GitHub Desktop application. If the login fails, the terminal may ask you to generate a token of authentication to use instead of a password. Here is a guide on how to create one.

With a successful response from the terminal, I could see my bragdoc live!

Maintaining your bragdoc

Unlike reports and books, a bragdoc must be maintained continuously as a live record of your progress and achievements. Think of your bragdoc like a garden, where tending requires regular attention and care. While you may not see the benefits straight away, time invested in tending to your document will lead to far greater returns. Instant recall and the ability to share what you’ve done are some of the upsides in forming this habit.

While you may not be able to note down everything as it happens, Julia Evans suggests setting a block of time to review your progress and update the document. Perhaps even making it a bi-weekly group activity to celebrate all wins, big and small.

For many, the less time it takes to do something, the better. With this bragdoc setup, adding new entries and rebuilding the site doesn’t take long at all! Just to give you an idea of how simple this is, I’ll walk you through the process of adding another entry to round out the tutorial.

Add a new bragdoc entry

Continuing from my last deployment, I’ll first add a new Markdown file in my posts folder.

eleventy-bragdoc ├── src │   ├── _data │   │   └── data.json │   ├── _filters │   │   └── dates.js │   ├── posts │   │   ├── posts.json │   │   ├── post-1.md │   │   ├── post-2.md │   │   ├── post-3.md │   │   └── post-4.md  // new entry goes here │   ├── css │   │   └── styles.css │   ├── images │   │   ├── profile_picture.jpg │   │   └── test_image.jpg │   ├── _includes │   │   ├── partials │   │   │   └── bragdoc-entry.njk │   │   └── layouts │   │       └── base.njk │   └── index.njk ├── .eleventy.js ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── .gitignore

Inside post-4.md, I’ll add in my front matter and description content.

--- title: Working towards publishing my first article on CSS-Tricks date: 2021-10-02 categories:   - Writing   - Eleventy public: True icon: ✍🏻 ---  Since re-creating [bragdocs.com](https://www.bragdocs.com/) using Eleventy, I am now in the process of writing the steps on how I did it. 

Run the build

With the entries added and saved, I’m ready to tell Eleventy to reference my Markdown files from src to generate static HTML files in the public folder. So I navigate the terminal to eleventy-bragdoc where I run the following command:

npm run-script build

Run deploy

Since I’ve already deployed once before, my GitHub credentials should grant me immediate access for deployment when running the following command:

npm run deploy

Those changes are then reflected on my website at the same configured URL.

What’s next?

Well first off, congratulations on putting together your very own bragdoc from scratch! It’s yours to keep, to tend and to share.

While this tutorial has only scratched the surface of what’s possible with Eleventy, a small step can lead you to all sorts of directions. To fuel your curiosity, check out what others are doing with Eleventy.

Feel free to reach out, I’d love to see what you come up with!

The post Creating Your Own Bragdoc With Eleventy appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.


, ,

Give your Eleventy Site Superpowers with Environment Variables

Eleventy is increasing in popularity because it allows us to create nice, simple websites, but also — because it’s so developer-friendly. We can build large-scale, complex projects with it, too. In this tutorial we’re going to demonstrate that expansive capability by putting a together a powerful and human-friendly environment variable solution.

What are environment variables?

Environment variables are handy variables/configuration values that are defined within the environment that your code finds itself in.

For example, say you have a WordPress site: you’re probably going to want to connect to one database on your live site and a different one for your staging and local sites. We can hard-code these values in wp-config.php but a good way of keeping the connection details a secret and making it easier to keep your code in source control, such as Git, is defining these away from your code.

Here’s a standard-edition WordPress wp-config.php snippet with hardcoded values:

<?php   define( 'DB_NAME', 'my_cool_db' ); define( 'DB_USER', 'root' ); define( 'DB_PASSWORD', 'root' ); define( 'DB_HOST', 'localhost' );

Using the same example of a wp-config.php file, we can introduce a tool like phpdotenv and change it to something like this instead, and define the values away from the code:

<?php   $ dotenv = DotenvDotenv::createImmutable(__DIR__); $ dotenv->load();  define( 'DB_NAME', $ _ENV['DB_NAME'] ); define( 'DB_USER', $ _ENV['DB_USER'] ); define( 'DB_PASSWORD', $ _ENV['DB_PASSWORD'] ); define( 'DB_HOST', $ _ENV['DB_HOST'] );

A way to define these environment variable values is by using a .env file, which is a text file that is commonly ignored by source control.

Example of a dot env file showing variables for a node environment, port, API key and API URL.

We then scoop up those values — which might be unavailable to your code by default, using a tool such as dotenv or phpdotenv. Tools like dotenv are super useful because you could define these variables in an .env file, a Docker script or deploy script and it’ll just work — which is my favorite type of tool!

The reason we tend to ignore these in source control (via .gitignore) is because they often contain secret keys or database connection information. Ideally, you want to keep that away from any remote repository, such as GitHub, to keep details as safe as possible.

Getting started

For this tutorial, I’ve made some starter files to save us all a bit of time. It’s a base, bare-bones Eleventy site with all of the boring bits done for us.

Step one of this tutorial is to download the starter files and unzip them wherever you want to work with them. Once the files are unzipped, open up the folder in your terminal and run npm install. Once you’ve done that, run npm start. When you open your browser at http://localhost:8080, it should look like this:

A default experience of standard HTML content running in localhost with basic styling

Also, while we’re setting up: create a new, empty file called .env and add it to the root of your base files folder.

Creating a friendly interface

Environment variables are often really shouty, because we use all caps, which can get irritating. What I prefer to do is create a JavaScript interface that consumes these values and exports them as something human-friendly and namespaced, so you know just by looking at the code that you’re using environment variables.

Let’s take a value like HELLO=hi there, which might be defined in our .env file. To access this, we use process.env.HELLO, which after a few calls, gets a bit tiresome. What if that value is not defined, either? It’s handy to provide a fallback for these scenarios. Using a JavaScript setup, we can do this sort of thing:

require('dotenv').config();  module.exports = {   hello: process.env.HELLO || 'Hello not set, but hi, anyway 👋' };

What we are doing here is looking for that environment variable and setting a default value, if needed, using the OR operator (||) to return a value if it’s not defined. Then, in our templates, we can do {{ env.hello }}.

Now that we know how this technique works, let’s make it happen. In our starter files folder, there is a directory called src/_data with an empty env.js file in it. Open it up and add the following code to it:

require('dotenv').config();  module.exports = {   otherSiteUrl:     process.env.OTHER_SITE_URL || 'https://eleventy-env-vars-private.netlify.app',   hello: process.env.HELLO || 'Hello not set, but hi, anyway 👋'   };

Because our data file is called env.js, we can access it in our templates with the env prefix. If we wanted our environment variables to be prefixed with environment, we would change the name of our data file to environment.js . You can read more on the Eleventy documentation.

We’ve got our hello value here and also an otherSiteUrl value which we use to allow people to see the different versions of our site, based on their environment variable configs. This setup uses Eleventy JavaScript Data Files which allow us to run JavaScript and return the output as static data. They even support asynchronous code! These JavaScript Data Files are probably my favorite Eleventy feature.

Now that we have this JavaScript interface set up, let’s head over to our content and implement some variables. Open up src/index.md and at the bottom of the file, add the following:

Here’s an example: The environment variable, HELLO is currently: “{{ env.hello }}”. This is called with {% raw %}{{ env.hello }}{% endraw %}. 

Pretty cool, right? We can use these variables right in our content with Eleventy! Now, when you define or change the value of HELLO in your .env file and restart the npm start task, you’ll see the content update.

Your site should look like this now:

The same page as before with the addition of content which is using environment variables

You might be wondering what the heck {% raw %} is. It’s a Nunjucks tag that allows you to define areas that it should ignore. Without it, Nunjucks would try to evaluate the example {{ env.hello }} part.

Modifying image base paths

That first example we did was cool, but let’s really start exploring how this approach can be useful. Often, you will want your production images to be fronted-up with some sort of CDN, but you’ll probably also want your images running locally when you are developing your site. What this means is that to help with performance and varied image format support, we often use a CDN to serve up our images for us and these CDNs will often serve images directly from your site, such as from your /images folder. This is exactly what I do on Piccalilli with ImgIX, but these CDNs don’t have access to the local version of the site. So, being able to switch between CDN and local images is handy.

The solution to this problem is almost trivial with environment variables — especially with Eleventy and dotenv, because if the environment variables are not defined at the point of usage, no errors are thrown.

Open up src/_data/env.js and add the following properties to the object:

imageBase: process.env.IMAGE_BASE || '/images/', imageProps: process.env.IMAGE_PROPS,

We’re using a default value for imageBase of /images/ so that if IMAGE_BASE is not defined, our local images can be found. We don’t do the same for imageProps because they can be empty unless we need them.

Open up _includes/base.njk and, after the <h1>{{ title }}</h1> bit, add the following:

<img src="https://assets.codepen.io/174183/mountains.jpg?width=1275&height=805&format=auto&quality=70" alt="Some lush mountains at sunset" /> 

By default, this will load /images/mountains.jpg. Cool! Now, open up the .env file and add the following to it:

IMAGE_BASE=https://assets.codepen.io/174183/ IMAGE_PROPS=?width=1275&height=805&format=auto&quality=70

If you stop Eleventy (Ctrl+C in terminal) and then run npm start again, then view source in your browser, the rendered image should look like this:

<img src="https://assets.codepen.io/174183/mountains.jpg?width=1275&height=805&format=auto&quality=70" alt="Some lush mountains at sunset" />

This means we can leverage the CodePen asset optimizations only when we need them.

Powering private and premium content with Eleventy

We can also use environment variables to conditionally render content, based on a mode, such as private mode. This is an important capability for me, personally, because I have an Eleventy Course, and CSS book, both powered by Eleventy that only show premium content to those who have paid for it. There’s all-sorts of tech magic happening behind the scenes with Service Workers and APIs, but core to it all is that content can be conditionally rendered based on env.mode in our JavaScript interface.

Let’s add that to our example now. Open up src/_data/env.js and add the following to the object:

mode: process.env.MODE || 'public'

This setup means that by default, the mode is public. Now, open up src/index.md and add the following to the bottom of the file:

{% if env.mode === 'private' %}  ## This is secret content that only shows if we’re in private mode.  This is called with {% raw %}`{{ env.mode }}`{% endraw %}. This is great for doing special private builds of the site for people that pay for content, for example.  {% endif %}

If you refresh your local version, you won’t be able to see that content that we just added. This is working perfectly for us — especially because we want to protect it. So now, let’s show it, using environment variables. Open up .env and add the following to it:


Now, restart Eleventy and reload the site. You should now see something like this:

The same page, with the mountain image and now some added private content

You can run this conditional rendering within the template too. For example, you could make all of the page content private and render a paywall instead. An example of that is if you go to my course without a license, you will be presented with a call to action to buy it:

A paywall that encourages the person to buy the content while blocking it

Fun mode

This has hopefully been really useful content for you so far, so let’s expand on what we’ve learned and have some fun with it!

I want to finish by making a “fun mode” which completely alters the design to something more… fun. Open up src/_includes/base.njk, and just before the closing </head> tag, add the following:

{% if env.funMode %}   <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Lobster&display=swap" />   <style>     body {       font-family: 'Comic Sans MS', cursive;       background: #fc427b;       color: #391129;     }     h1,     .fun {       font-family: 'Lobster';     }     .fun {       font-size: 2rem;       max-width: 40rem;       margin: 0 auto 3rem auto;       background: #feb7cd;       border: 2px dotted #fea47f;       padding: 2rem;       text-align: center;     }   </style> {% endif %}

This snippet is looking to see if our funMode environment variable is true and if it is, it’s adding some “fun” CSS.

Still in base.njk, just before the opening <article> tag, add the following code:

{% if env.funMode %}   <div class="fun">     <p>🎉 <strong>Fun mode enabled!</strong> 🎉</p>   </div> {% endif %}

This code is using the same logic and rendering a fun banner if funMode is true. Let’s create our environment variable interface for that now. Open up src/_data/env.js and add the following to the exported object:

funMode: process.env.FUN_MODE

If funMode is not defined, it will act as false, because undefined is a falsy value.

Next, open up your .env file and add the following to it:


Now, restart the Eleventy task and reload your browser. It should look like this:

The main site we’re working on but now, it’s bright pink with Lobster and Comic Sans fonts

Pretty loud, huh?! Even though this design looks pretty awful (read: rad), I hope it demonstrates how much you can change with this environment setup.

Wrapping up

We’ve created three versions of the same site, running the same code to see all the differences:

  1. Standard site
  2. Private content visible
  3. Fun mode

All of these sites are powered by identical code with the only difference between each site being some environment variables which, for this example, I have defined in my Netlify dashboard.

I hope that this technique will open up all sorts of possibilities for you, using the best static site generator, Eleventy!

The post Give your Eleventy Site Superpowers with Environment Variables appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

Cloudinary Fetch with Eleventy (Respecting Local Development)

This is about a wildly specific combination of technologies — Eleventy, the static site generator, with pages with images on them that you ultimately want hosted by Cloudinary — but I just wanna document it as it sounds like a decent amount of people run into this situation.

The deal:

  • Cloudinary has a fetch URL feature, meaning you don’t actually have to learn anything (nice!) to use their service. You have to have an account, but after that you just prefix your images with a Cloudinary URL and then it is Cloudinary that optimizes, resizes, formats, and CDN serves your image. Sweet. It’s not the only service that does this, but it’s a good one.
  • But… the image needs to be on the live public internet. In development, your image URLs probably are not. They’re likely stored locally. So ideally we could keep using local URLs for images in development, and do the Cloudinary fetching on production.

Multiple people have solved this in different ways. I’m going to document how I did it (because I understand it best), but also link up how several other people have done it (which might be smarter, you be the judge).

The goal:

  • In development, images be like /images/image.png
  • In production, images be like https://res.cloudinary.com/css-tricks/image/fetch/w_1200,q_auto,f_auto/https://production-website.com/images/image.png

So if we were to template that (let’s assume Nunjucks here as it’s a nice templating language that Eleventy supports), we get something like this psuedo-code:

<img src="   {{CLOUDINARY_PREFIX}}{{FULLY_QUALIFIED_PRODUCTION_URL}}{{RELATIVE_IMAGE_URL}}   "   alt="Don't screw this up, fam." />
Development Production
{{CLOUDINARY_PREFIX}} “” “https://res.cloudinary.com/css-tricks/image/fetch/w_1200,q_auto,f_auto/”
{{FULLY_QUALIFIED_PRODUCTION_URL}} “” “https://production-website.com”
{{RELATIVE_IMAGE_URL}} “/images/image.jpg” “/images/image.jpg”

The trick then is getting those… I guess we’ll call them global variables?… set up. It’s probably just those first two. The relative image path you’d likely just write by hand as needed.

Eleventy has some magic available for this. Any *.js file we put in a _data folder will turn into variables we can use in templates. So if we made like /src/_data/sandwiches.js and it was:

module.exports = {   ham: true }

In our template, we could use {{sandwiches.ham}} and that would be defined {{true}}.

Because this is JavaScript (Node), that means we have the ability to do some logic based on other variables. In our case, some other global variables will be useful, particularly the process.env variables that Node makes available. A lot of hosts (Netlify, Vercel, etc.) make “environment variables” a thing you can set up in their system, so that process.env has them available when build processes run on their system. We could do that, but that’s rather specific and tied to those hosts. Another way to set a Node global variable is to literally set it on the command line before you run a command, so if you were to do:

SANDWICH="ham" eleventy

Then process.env.SANDWICH would be ham anywhere in your Node JavaScript. Combining all that… let’s say that our production build process sets a variable indicating production, like:

PROD="true" eleventy

But on local development, we’ll run without that global variable. So let’s make use of that information while setting up some global variables to use to construct our image sources. In /src/_data/images.js (full real-world example) we’ll do:

module.exports = {    imageLocation:     process.env.PROD === 'true'        ? 'https://coding-fonts.css-tricks.com'        : '',    urlPrefix:     process.env.PROD === 'true'       ? 'https://res.cloudinary.com/css-tricks/image/fetch/w_1600,q_auto,f_auto/'       : ''  };

You could also check process.env.CONTEXT === 'deploy-preview' to test for Netlify deploy preview URLs, in case you want to change the logic there one way or the other.

Now in any of our templates, we can use {{images.imageLocation}} and {{images.urlPrefix}} to build out the sources.

<img    src="     {{images.urlPrefixLarge}}{{images.imageLocation}}/image.png   "   alt="Useful alternative text." />

And there we go. That will be a local/relative source on development, and then on production, it becomes this prefixed and full qualified URL from which Cloudinary’s fetch will work.

Now that it’s on Cloudinary, we can take it a step further. The prefix URL can be adjusted to resize images, meaning that even with just one source image, we can pull off a rather appropriate setup for responsive images. Here’s that setup, which makes multiple prefixes available, so they can be used for the full syntax.

The end result means locally relative image in development:

The multiple versions are a lie in development, but oh well, srcset is kind of a production concern.

…and Cloudinary fetch URLs in production:

Other People’s Ideas

Phil was showing off using Netlify redirects to do this the other day:

Then the trick to local development is catching the 404’s and redirecting them locally with more redirects.

If hand-crafting your own responsive images syntax is too big of a pain (it is), I highly recommend abstracting it. In Eleventy-land, Nicolas Hoizey has a project: eleventy-plugin-images-responsiver. Eric Portis has one as well, eleventy-respimg, which specifically uses Cloudinary as I have here.

Proving this stuff has really been on people’s minds, Tim Kadlec just blogged “Proxying Cloudinary Requests with Netlify.” He expands on Phil’s tweet, adding some extra performance context and gotchas.

The post Cloudinary Fetch with Eleventy (Respecting Local Development) appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , , ,

A Community-Driven Site with Eleventy: Building the Site

In the last article, we learned what goes into planning for a community-driven site. We saw just how many considerations are needed to start accepting user submissions, using what I learned from my experience building Style Stage as an example.

Now that we’ve covered planning, let’s get to some code! Together, we’re going to develop an Eleventy setup that you can use as a starting point for your own community (or personal) site.

Article Series:

  1. Preparing for Contributions
  2. Building the Site (You are here!)

This article will cover:

  • How to initialize Eleventy and create useful develop and build scripts
  • Recommended setup customizations
  • How to define custom data and combine multiple data sources
  • Creating layouts with Nunjucks and Eleventy layout chaining
  • Deploying to Netlify

The vision

Let’s assume we want to let folks submit their dogs and cats and pit them against one another in cuteness contests.

Screenshot of the site, showing a Meow vs. Bow Wow heading above a Weekly Battle subheading, followed by a photo of a tabby cat named Fluffy and one of a happy dog named Lexi.
Live demo

We’re not going to get into user voting in this article. That would be so cool (and totally possible with serverless functions) but our focus is on the pet submissions themselves. In other words, users can submit profile details for their cats and dogs. We’ll use those submissions to create a weekly battle that puts a random cat up against a random dog on the home page to duke it out over which is the most purrrfect (or woof-tastic, if you prefer).

Let’s spin up Eleventy

We’ll start by initializing a new project by running npm init on any directory you’d like, then installing Eleventy into it with:

npm install @11ty/eleventy

While it’s totally optional, I like to open up the package-json file that’s added to the directory and replace the scripts section with this:

"scripts": {   "develop": "eleventy --serve",   "build": "eleventy" },

This allows us to start developing Eleventy in a development environment (npm run develop) that includes Browsersync hot-reloading for local development. It also adds a command that compiles and builds our work (npm run build) for deployment on a production server.

If you’re thinking, “npm what?” what we’re doing is calling on Node (which is something Eleventy requires). The commands noted here are intended to be run in your preferred terminal, which may be an additional program or built-in to your code editor, like it is in VS Code.

We’ll need one more npm package, fast-glob, that will come in handy a little later for combining data. We may as well install it now:

npm install --save-dev fast-glob.

Let’s configure our directory

Eleventy allows customizing the input directory (where we work) and output directory (where our built work goes) to provide a little extra organization.

To configure this, we’ll create the eleventy.js file at the root of the project directory. Then we’ll tell Eleventy where we want our input and output directories to go. In this case, we’re going to use a src directory for the input and a public directory for the output.

module.exports = function (eleventyConfig) {   return      dir: {       input: "src",       output: "public"     },   }; };

Next, we’ll create a directory called pets where we’ll store the pets data we get from user submissions. We can even break that directory down a little further to reduce merge conflicts and clearly distinguish cat data from dog data with cat and dog subdirectories:

pets/   cats/   dogs/

What’s the data going to look like? Users will send in a JSON file that follows this schema, where each property is a data point about the pet:

{   "name": "",   "petColor": "",   "favoriteFood": "",   "favoriteToy": "",   "photoURL": "",   "ownerName": "",   "ownerTwitter": "" }

To make the submission process crystal clear for users, we can create a CONTRIBUTING.md file at the root of the project and write out the guidelines for submissions. GitHub takes the content in this file and uses displays it in the repo. This way, we can provide guidance on this schema such as a note that favoriteFood, favoriteToy, and ownerTwitte are optional fields.

A README.md file would be just as fine if you’d prefer to go that route. It’s just nice that there’s a standard file that’s meant specifically for contributions.

Notice photoURL is one of those properties. We could’ve made this a file but, for the sake of security and hosting costs, we’re going to ask for a URL instead. You may decide that you are willing to take on actual files, and that’s totally cool.

Let’s work with data

Next, we need to create a combined array of data out of the individual cat files and dog files. This will allow us to loop over them to create site pages and pick random cat and dog submissions for the weekly battles.

Eleventy allows node module.exports within the _data directory. That means we can create a function that finds all cat files and another that finds all dog files and then creates arrays out of each set. It’s like taking each cat file and merging them together to create one data set in a single JavaScript file, then doing the same with dogs.

The filename used in _data becomes the variable that holds that dataset, so we’ll add files for cats and dogs in there:

_data/   cats.js   dogs.js

The functions in each file will be nearly identical — we’re merely swapping instances of “cat” for “dog” between the two. Here’s the function for cats: 

const fastglob = require("fast-glob"); const fs = require("fs"); 
 module.exports = async () => {   // Create a "glob" of all cat json files   const catFiles = await fastglob("./src/pets/cats/*.json", {     caseSensitiveMatch: false,   }); 
   // Loop through those files and add their content to our `cats` Set   let cats = new Set();   for (let cat of catFiles) {     const catData = JSON.parse(fs.readFileSync(cat));     cats.add(catData);   } 
   // Return the cats Set of objects within an array   return [...cats]; };

Does this look scary? Never fear! I do not routinely write node either, and it’s not a required step for less complex Eleventy sites. If we had instead chosen to have contributors add to an ever growing single JSON file with _data, then this combination step wouldn’t be necessary in the first place. Again, the main reason for this step is to reduce merge conflicts by allowing for individual contributor files. It’s also the reason we added fast-glob to the mix.

Let’s output the data

This is a good time to start plugging data into the templates for our UI. In fact, go ahead and drop a few JSON files into the pets/cats and pets/dogs directories that include data for the properties so we have something to work with right out of the gate and test things.

We can go ahead and add our first Eleventy page by adding a index.njk file in the src directory. This will become the home page, and is a Nunjucks template file format.

Nunjucks is one option of many for creating templates with Eleventy. See the docs for a full list of templating options.

Let’s start by looping over our data and outputting an unordered list both for cats and dogs:

<ul>   <!-- Loop through cat data -->   {% for cat in cats %}   <li>     <a href="/cats/{{ cat.name | slug }}/">{{ cat.name }}</a>   </li>   {% endfor %} </ul> 
 <ul>   <!-- Loop through dog data -->   {% for dog in dogs %}   <li>     <a href="/dogs/{{ dog.name | slug }}/">{{ dog.name }}</a>   </li>   {% endfor %} </ul>

As a reminder, the reference to cats and dogs matches the filename in _data. Within the loop we can access the JSON keys using dot notation, as seen for cat.name, which is output as a Nunjucks template variable using double curly braces (e.g. {{ cat.name }}).

Let’s create pet profile pages

Besides lists of cats and dogs on the home page (index.njk), we also want to create individual profile pages for each pet. The loop indicated a hint at the structure we’ll use for those, which will be [pet type]/[name-slug].

The recommended way to create pages from data is via the Eleventy concept of pagination which allows chunking out data.

We’re going to create the files responsible for the pagination at the root of the src directory, but you could nest them in a custom directory, as long as it lives within src and can still be discovered by Eleventy.

src/   cats.njk   dogs.njk

Then we’ll add our pagination information as front matter, shown for cats:

--- pagination:   data: cats   alias: cat   size: 1 permalink: "/cats/{{ cat.name | slug }}/" ---

The data value is the filename from _data. The alias value is optional, but is used to reference one item from the paginated array. size: 1 indicates that we’re creating one page per item of data.

Finally, in order to successfully create the page output, we need to also indicate the desired permalink structure. That’s where the alias value above comes into play, which accesses the name key from the dataset. Then we are using a built-in filter called slug that transforms a string value into a URL-friendly string (lowercasing and converting spaces to dashes, etc).

Let’s review what we have so far

Now is the time to fire up Eleventy with npm run develop. That will start the local server and show you a URL in the terminal you can use to view the project. It will show build errors in the terminal if there are any.

As long as all was successful, Eleventy will create a public directory, which should contain:

public/   cats/     cat1-name/index.html     cat2-name/index.html   dogs/     dog1-name/index.html     dog2-name/index.html   index.html

And in the browser, the index page should display one linked list of cat names and another one of linked dog names.

Let’s add data to pet profile pages

Each of the generated pages for cats and dogs is currently blank. We have data we can use to fill them in, so let’s put it to work.

Eleventy expects an _includes directory that contains layout files (“templates”) or template partials that are included in layouts.

We’ll create two layouts:

src/   _includes/     base.njk     pets.njk

The contents of base.njk will be an HTML boilerplate. The <body> element in it will include a special template tag, {{ content | safe }}, where content passed into the template will render, with safe meaning it can render any HTML that is passed in versus encoding it.

Then, we can assign the homepage, index.md, to use the base.njk layout by adding the following as front matter. This should be the first thing in index.md, including the dashes:

--- layout: base.njk ---

If you check the compiled HTML in the public directory, you’ll see the output of the cat and dog loops we created are now within the <body> of the base.njk layout.

Next, we’ll add the same front matter to pets.njk to define that it will also use the base.njk layout to leverage the Eleventy concept of layout chaining. This way, the content we place in pets.njk will be wrapped by the HTML boilerplate in base.njk so we don’t have to write out that HTML each and every time.

In order to use the single pets.njk template to render both cat and dog profile data, we’ll use one of the newest Eleventy features called computed data. This will allow us to assign values from the cats and dogs data to the same template variables, as opposed to using if statements or two separate templates (one for cats and one for dogs). The benefit is, once again, to avoid redundancy.

Here’s the update needed in cats.njk, with the same update needed in dogs.njk (substituting cat with dog):

eleventyComputed:   title: "{{ cat.name }}"   petColor: "{{ cat.petColor }}"   favoriteFood: "{{ cat.favoriteFood }}"   favoriteToy: "{{ cat.favoriteToy }}"   photoURL: "{{ cat.photoURL }}"   ownerName: "{{ cat.ownerName }}"   ownerTwitter: "{{ cat.ownerTwitter }}"

Notice that eleventyComputed defines this front matter array key and then uses the alias for accessing values in the cats dataset. Now, for example, we can just use {{ title }} to access a cat’s name and a dog’s name since the template variable is now the same.

We can start by dropping the following code into pets.njk to successfully load cat or dog profile data, depending on the page being viewed:

<img src="{{ photoURL }}" /> <ul>   <li><strong>Name</strong>: {{ title }}</li>   <li><strong>Color</strong>: {{ petColor }}</li>   <li><strong>Favorite Food</strong>: {{ favoriteFood if favoriteFood else 'N/A' }}</li>   <li><strong>Favorite Toy</strong>: {{ favoriteToy if favoriteToy else 'N/A' }}</li> {% if ownerTwitter %}   <li><strong>Owner</strong>: <a href="{{ ownerTwitter }}">{{ ownerName }}</a></li> {% else %}   <li><strong>Owner</strong>: {{ ownerName }}</li> {% endif %} </ul>

The last thing we need to tie this all together is to add layout: pets.njk to the front matter in both cats.njk and dogs.njk.

With Eleventy running, you can now visit an individual pet page and see their profile:

Screenshot of a cat profile page that starts with the cat's name for the heading, followed by the cat's photo, and a list of the cat's details.
Fancy Feast for a fancy cat. 😻

We’re not going into styling in this article, but you can head over to the sample project repo to see how CSS is included.

Let’s deploy this to production!

The site is now in a functional state and can be deployed to a hosting environment! 

As recommended earlier, Netlify is an ideal choice, particularly for a community-driven site, since it can trigger a deployment each time a submission is merged and provide a preview of the submission before sending it for review.

If you choose Netlify, you will want to push your site to a GitHub repo which you can select during the process of adding a site to your Netlify account. We’ll tell Netlify to serve from the public directory and run npm run build when new changes are merged into the main branch.

The sample site includes a netlify.toml file which has the build details and is automatically detected by Netlify in the repo, removing the need to define the details in the new site flow.

Once the initial site is added, visit Settings → Build → Deploy in Netlify. Under Deploy contexts, select “Edit” and update the selection for “Deploy Previews” to “Any pull request against your production branch / branch deploy branches.” Now, for any pull request, a preview URL will be generated with the link being made available directly in the pull request review screen.

Let’s start accepting submissions!

Before we pass Go and collect $ 100, it’s a good idea to revisit the first post and make sure we’re prepared to start taking user submissions. For example, we ought to add community health files to the project if they haven’t already been added. Perhaps the most important thing is to make sure a branch protection rule is in place for the main branch. This means that your approval is required prior to a pull request being merged.

Contributors will need to have a GitHub account. While this may seem like a barrier, it removes some of the anonymity. Depending on the sensitivity of the content, or the target audience, this can actually help vet (get it?) contributors.

Here’s the submission process:

  1. Fork the website repository.
  2. Clone the fork to a local machine or use the GitHub web interface for the remaining steps.
  3. Create a unique .json file within src/pets/cats or src/pets/dogs that contains required data.
  4. Commit the changes if they’re made on a clone, or save the file if it was edited in the web interface.
  5. Open a pull request back to the main repository.
  6. (Optional) Review the Netlify deploy preview to verify information appears as expected.
  7. Merge the changes.
  8. Netlify deploys the new pet to the live site.

A FAQ section is a great place to inform contributors how to create pull request. You can check out an example on Style Stage.

Let’s wrap this up…

What we have is fully functional site that accepts user contributions as submissions to the project repo. It even auto-deploys those contributions for us when they’re merged!

There are many more things we can do with a community-driven site built with Eleventy. For example:

  • Markdown files can be used for the content of an email newsletter sent with Buttondown. Eleventy allows mixing Markdown with Nunjucks or Liquid. So, for example, you can add a Nunjucks for loop to output the latest five pets as links that output in Markdown syntax and get picked up by Buttondown.
  • Auto-generated social media preview images can be made for social network link previews.
  • A commenting system can be added to the mix.
  • Netlify CMS Open Authoring can be used to let folks make submissions with an interface. Check out Chris’ great rundown of how it works.

My Meow vs. BowWow example is available for you to fork on GitHub. You can also view the live preview and, yes, you really can submit your pet to this silly site. 🙂

Best of luck creating a healthy and thriving community!

Article Series:

  1. Preparing for Contributions
  2. Building the Site (You are here!)

The post A Community-Driven Site with Eleventy: Building the Site appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , ,

A Community-Driven Site with Eleventy: Preparing for Contributions

I’ve recently found myself reaching for Eleventy (aka 11ty) above all other tools when I want to develop a website. It’s hard to beat a static site generator that provides advanced templating opportunities while otherwise getting out of your way and allowing you to just create.

One of those sites is Style Stage, a modern CSS showcase styled by community contributions. Eleventy was perfect for this community-driven project in several ways:

  • Its exceptionally fast builds locally and on a production host
  • It’s un-opinionated about how to construct templates
  • Its ability to create any file type with complete control over how and where files are rendered
  • Its ability to intermix templating languages, such as HTML, Markdown, and Nunjucks
  • It’s highly performant because it compiles to static HTML with no required dependencies for production

The number one reason Eleventy is a great choice for creating a community-driven site is the ability to dynamically create site pages from data sources. We’ll review how to use this feature and more when we create our sample community site.

Article Series:

  1. Preparing for Contributions (You are here!)
  2. Building the Site (Coming tomorrow!)

What goes into creating a community-driven site?

In the not-so-distant past, creating a community-driven site could potentially be a painful process involving CMS nightmares trying to create contributor workflows. Armed with Eleventy and a few other modern tools, this is now nearly fully automatable with a minimum of oversight.

Before we get to inviting contributors, we’ve got some work to do ourselves.

1. Determine what content contributors will have access to modify

This will guide a lot of the other decisions. In the case of using Eleventy for Style Stage, I created a JSON file that contributors can use to create pull requests to modify and provide their own relevant metadata that’s used to create their pages.

An early version of the JSON file which initially had an “Example” for contributors to reference. This screenshot also shows the first two contributors details.

Perhaps you also want to allow access to include additional assets, or maybe it makes sense to have multiple data files for the ease of categorizing and querying data. Or maybe contributors are able to add Markdown files within a particular directory.

Consider the scope of what contributors can modify or submit, and weigh that against an estimate of your availability to review submissions. This will help enable a successful, manageable community.

GitHub actions can make it possible to label or close a pull request with invalid files if you need advanced automated screening of incoming content.

2. Create contributor guidelines

Spending time upfront to think through your guidelines can help with your overall plan. You may identify additional needed features, or items that can be automated.

Once your guidelines are prepared, it’s best to include them in a special file in your GitHub repository called CONTRIBUTING.md. The all-caps filename is the expected format. Having this file creates an automatic extra link for contributors when they are creating their pull request or issues in a prompt that ask them to be sure they’ve reviewed the guidelines:

Screenshot courtesy of the GitHub documentation.

How to handle content licensing and author attribution are things that fall into this category. For example, Style Stage releases contributed stylesheets under the CC BY-NC-SA license but authors retain copyright over original graphics. As part of the build process, the license and author attribution are appended to the styles, and the authors attribution metadata is updated within the style page template.

You’ll also want to consider policies around acceptable content and what would cause submissions to be rejected. Style Stage states that:

Submissions will be rejected for using obscene, excessively violent, or otherwise distasteful imagery, violating the above guidelines, or other reasons at the discretion of the maintainer.

3. Prepare workflow and automations

While Eleventy takes care of the site build, the other key players enabling Style Stage contributions are Netlify and GitHub.

Contributors submit a pull request to the Style Stage repo on GitHub and, when they do, Netlify creates a deploy preview. This allows contributors to verify that their submission works as expected, and saves me time as the maintainer by not having to pull down submissions to ensure they meet the guidelines.

The status of the Netlify deploy updates in real-time on the pull request review page. Once the last item (“/deploy-preview”) displays “Deploy preview ready!” clicking “Details” will launch the live link to the preview.

All discussion takes place through GitHub. This has the added advantage of public accountability which helps dissuade bad actors.

If the contributor needs to make a change, they can update their pull request or request a re-deploy of the branch preview if it’s a remote asset that has changed. This re-deploy is a very small manual step, and it may not be needed for every PR — or even at all, depending on how you accept contributions.

The last step is the final approval of the PR and merging into the main branch. Once the pull request is merged, Netlify immediately deploys the changes to production.

Eleventy is, of course, a static site generator, and several hosts offer webhooks to trigger a build. Netlify’s build plugins are a good example of that. But if you need to refresh data more often than each time a PR is merged, one option is to use IFTTT or Zapier to set up daily deploys, or deploys based on a variety of other triggers.

Example of completed setup of a daily deploy via webhook from IFTTT

It’s worth noting that what we’re talking about here does limit your contributor audience to having a GitHub account. However, GitHub contributions can be done entirely via the web interface, so it’s very possible to provide guidance so that other users — even those who don’t code — can still participate.

4. Choose a method for contributor and community updates

The first consideration here is to decide how critical it is for contributors to know about updates to your site by evaluating the likely impact of the change.

In the case of Style Stage, the core will be unchanging, but there is some planned optional functionality. I went with a weekly(-ish) newsletter. That way, it is something folks can opt into and there is value for contributors and users alike.

Matthew Ström’s “Using Netlify Forms and Netlify Functions to Build an Email Sign-Up Widget” is a great place to learn how to add subscribers to your newsletter with a static form in Eleventy. It also covers a function for sending the subscriber’s email to Buttondown, a lightweight email service. For an example of how to manage your Buttondown email template and content in Eleventy, review the Style Stage setup which shows how to exclude the newsletter from the published site build.

If you’re only expecting low priority updates, then GitHub’s repo notifications might be sufficient for communication. Creating releases is another way to go. Or, hey, it’s even possible to to incorporate notifications on the site itself.

5. Find and engage with potential contributors

Style Stage was an idea that I vetted by tossing out a poll on Twitter. I then put out a “call for contributors” and engaged with responders as well as those who retweeted me. A short timeline also helped find motivated contributors who helped Style Stage avoid launching without any submissions. Many of those contributors became evangelists that introduced Style Stage to even more people. I also promoted a launch livestream which doubled as promotional material.

This is what it means to “engage” with contributors. Creating avenues for engagement and staying engaged with them helps turn casual contributors into “fans” who encourage others to participate.

Remember that the site content is a great place to encourage participation! Style Stage dedicates its entire page to encouraging submissions. If that’s not possible for you, then you might consider using prompts for contributions where it makes sense.

6. Finalize repo settings and include community health files

Finally, ensure that your repository is published publicly and that it includes applicable “community health” files. These are meant to be documents that help establish guidelines, set good expectations with community members, define a code of conduct, and other information that contribute to the overall “health” of the community. There are a bunch of examples, suggestions and tips on how to do this in the GitHub docs.

While there are a half dozen files noted in the documentation, in my experience so far, the three files you’ll need at minimum are:

  • a README.md file at the root of the project that includes the project’s name and a good description of what it is. GitHub will display the contents below the list of files in the repo.
  • a CONTRIBUTING.md file that describes the submission process for contributions. Be explicit as far as what steps are involved and what constitutes a “good” submission.
  • a pull request template. I wouldn’t exactly say this is a mandatory thing, but it’s worth adding to this list because it further solidifies the expectations for submitting contributions. Many templates will even include a checklist that details requirements for approval.

Oh, and having a branch protection rule on the main branch is another good idea. You can do this by going to SettingsBranches from the repo and selecting the “Add rule” option. “Require pull request reviews before merging” and “Require review from Code Owners” are the two key settings to enable. You can check the GitHub docs to learn more about this protection.

Coming up next…

What we covered here is a starting point for creating a community-driven site with Eleventy. The point is that there are several things that need to be considered before we jump straight into code. Communities need care and that requires a few steps that help establish an engaged and healthy community.

You’re probably getting anxious to start coding a community site with Eleventy! Well, that’s coming up in the next installment of this two-parter.  Together, we’ll develop an Eleventy starter from scratch that you can extend for your own community (or personal) site.

Article Series:

  1. Preparing for Contributions (You are here!)
  2. Building the Site (Coming tomorrow!)

The post A Community-Driven Site with Eleventy: Preparing for Contributions appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.


, , , ,

An Eleventy Starter with Tailwind CSS and Alpine.js

When I decided to try to base my current personal website on Eleventy, I didn’t want to reinvent the wheel: I tested all the Eleventy starters built with Tailwind CSS that I could find in Starter Projects from the documentation.

Many of the starters seemed to integrate Tailwind CSS in a contrived way. Also, some of them seemed to assume that no one updates Tailwind’s configuration on the fly while working on a website. That’s why I integrated Eleventy with Tailwind CSS and Alpine.js myself. I have reason to believe that you’ll like the simplicity of my solution.

Good design is as little design as possible.

—Dieter Rams, 10 Principles for Good Design

If you’re uninterested in the details, feel free to grab my starter and jump right in.

Getting started

I’m going to assume you have a general understanding of Tailwind CSS, HTML, JavaScript, Nunjucks, the command line, and npm.

Let’s start by with a new a folder, then cd to it in the command line, and initialize it with a package.json file:

npm init -y

Now we can install Eleventy and Tailwind CSS. We’ll throw in PostCSS as well:

npm install --save-dev @11ty/eleventy tailwindcss postcss-cli

We need to create a page to test whether we’ve successfully set things up. In a real use case, our pages will use templates, so we’ll do that here as well. That’s where Nunjucks fits into the mix, serving as a templating engine.

Let’s make a new file called index.njk in the project folder. We’ll designate it as the homepage:

{% extends "_includes/default.njk" %} 
 {% block title %}It does work{% endblock %} 
 {% block content %}   <div class="fixed inset-0 flex justify-center items-center">     <div>       <span class="text-change">Good design</span><br/>       <span class="change">is<br/>as little design<br/>as possible</span>     </div>   </div> {% endblock %}

Basic templating

Now let’s create a new folder in the project folder called _includes (and yes, the folder name matters). Inside this new folder, we’ll create a file called default.njk that we’ll use as the default template for our layout. We’ll keep things simple with a basic HTML boilerplate:

<!DOCTYPE html> <html lang="en">   <head>     <title>       {% block title %}Does it work?{% endblock %}     </title>     <meta charset="UTF-8"/>     {% if description %}       <meta name="description" content="{{description}}"/>     {% endif %}     <meta http-equiv="x-ua-compatible" content="ie=edge"/>     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>     <link rel="stylesheet" href="/style.css?v={% version %}"/>     {% block head %}{% endblock %}   </head>   <body>     {% block content %}       {{ content | safe }}     {% endblock %}   </body> </html>

Configuring Tailwind CSS

Let’s take care of a test for Tailwind CSS in as few moves as possible. First, create a new subfolder called styles and a file in it called tailwind.config.js:

module.exports = {   theme: {     colors: {       change: "transparent"     }   },   variants: {},   plugins: [], }

Then, create a file called tailwind.css in that same styles folder:

/* purgecss start ignore */ @tailwind base; /* purgecss end ignore */ 
 .change {   color: transparent; } 
 /* purgecss start ignore */ @tailwind components; /* purgecss end ignore */ 
 @tailwind utilities;

We’re done with the styles folder for now. What we do need is a configuration file that tells PostCSS to use Tailwind CSS, which we can get by creating a new file in the root directory of the project folder called postcss.config.js. Here’s how we require Tailwind CSS and its configuration file with PostCSS:

module.exports = {   plugins: [     require(`tailwindcss`)(`./styles/tailwind.config.js`)   ], };

Starting and building the project

Now let’s create another new file in the same root directory called .gitignore. This will allow us to define what files to skip when committing the project to a repo, like on GitHub:

_site/ _tmp/ .DS_Store node_modules/ package-lock.json

Next, is another new file, this time one that tells Eleventy what it can ignore, called .eleventyignore. It only needs one line:


OK, now we will create a file called .eleventy.js (note the leading dot!) that basically configures Eleventy, telling it what files to watch and where to save its work:

module.exports = function (eleventyConfig) {   eleventyConfig.setUseGitIgnore(false); 
   eleventyConfig.addPassthroughCopy({ "./_tmp/style.css": "./style.css" }); 
   eleventyConfig.addShortcode("version", function () {     return String(Date.now());   }); };

We can now update the package.json file with all of the scripts we need to start and build the site during development. The dependencies should already be there from the initial setup.

{   "scripts": {     "start": "eleventy --serve & postcss styles/tailwind.css --o _tmp/style.css --watch",     "build": "ELEVENTY_PRODUCTION=true eleventy & ELEVENTY_PRODUCTION=true postcss styles/tailwind.css --o _site/style.css"   },   "devDependencies": {     "@11ty/eleventy": "^0.11.0",     "postcss-cli": "^7.1.0",     "tailwindcss": "^1.4.6"   } }

Hey, great job! We made it. Build the project to generate the initial CSS — this step is only required the very first time we set up. From the command line:

npm run build

And — drumroll, please — let’s officially start the site:

npm run start

Open the page http://localhost:8080 in your browser. It’s not gonna look like much, but check out the page title in the browser tab:

It does work!

We can still do a little more checking to make sure everything’s good. Open up /styles/tailwind.config.js and change the transparent color value to something else, say black. Tailwind’s configuration should reload, along with the page in your browser.

Don’t lose sight of your browser and edit /styles/tailwind.css by changing transparent to black again. Your CSS file should reload and refresh in your browser.

Now we can work nicely with Eleventy and Tailwind CSS!

Optimizing the output

At this point, Tailwind CSS works with Eleventy, but the CSS file is huge. The generated HTML isn’t perfect either because it contains stuff like redundant newline characters. Let’s clean it up:

npm install --save-dev @fullhuman/postcss-purgecss postcss-clean html-minifier

Open up postcss.config.js and replace what’s in there with this:

module.exports = {   plugins: [     require(`tailwindcss`)(`./styles/tailwind.config.js`),     require(`autoprefixer`),     ...(process.env.ELEVENTY_PRODUCTION       ? [           require(`postcss-clean`),           require(`@fullhuman/postcss-purgecss`)({             content: ["_site/**/*.html"],             defaultExtractor: (content) =>               content.match(/[w-/:]+(?<!:)/g) || [],             whitelist: [],             whitelistPatterns: [/body/, /headroom/, /ril/],           }),         ]       : []),   ], };

In the future, in the process of creating your website, if something looks wrong after you build the project and you have the impression that fragments of CSS are missing, add “ignored” class names to the whitelist in the file above.

Add the following line to the beginning of the .eleventy.js file:

const htmlmin = require("html-minifier");

We also need to configure htmlmin in .eleventy.js as well:

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

We’re using a transform here which is an Eleventy thing. Transforms can modify a template’s output. At this point, .eleventy.js should look like this:

const htmlmin = require("html-minifier"); 
 module.exports = function (eleventyConfig) {   eleventyConfig.setUseGitIgnore(false); 
   eleventyConfig.addPassthroughCopy({ "./_tmp/style.css": "./style.css" }); 
   eleventyConfig.addShortcode("version", function () {     return String(Date.now());   }); 
   eleventyConfig.addTransform("htmlmin", function (content, outputPath) {     if (       process.env.ELEVENTY_PRODUCTION &&       outputPath &&       outputPath.endsWith(".html")     ) {       let minified = htmlmin.minify(content, {         useShortDoctype: true,         removeComments: true,         collapseWhitespace: true,       });       return minified;     } 
     return content;   }); };

Alright, let’s run npm run start once again. You’ll see that nothing has changed and that’s because optimization only happens during build. So, instead, let’s try npm run build and then look at the _site folder. There shouldn’t be a single unnecessary character in the index.html file. The same goes for the style.css file.

A project built like this is now ready to deploy. Good job! 🏆

Integrating Alpine.js

I decided to switch to Eleventy from Gatsby.js because it just felt like too much JavaScript to me. I’m more into the reasonable dose of vanilla JavaScript mixed with Alpine.js. We won’t get into the specifics of Alpine.js here, but it’s worth checking out Hugo DiFrancesco’s primer because it’s a perfect starting point.

Here’s how we can install it to our project from the command line:

npm install --save-dev alpinejs

Now we need to update .eleventy.js with this to the function that passes things through Alpine.js:

eleventyConfig.addPassthroughCopy({   "./node_modules/alpinejs/dist/alpine.js": "./js/alpine.js", }); 

Lastly, we’ll open up _includes/default.njk and add Alpine.js right before the closing </head> tag:

<script src="/js/alpine.js?v={% version %}"></script>

We can check if Alpine is working by adding this to index.njk:

{% extends "_includes/default.njk" %} 
 {% block title %}It does work{% endblock %} 
 {% block content %}   <div class="fixed inset-0 flex justify-center items-center">     <div>       <span class="text-change">Good design</span><br/>       <span class="change">is<br/>as little design<br/>as possible</span><br/>       <span x-data="{message:'🤖 Hello World 🤓'}" x-text="message"></span>     </div>   </div> {% endblock %}

Launch the project:

npm run start

If Alpine.js works, you’ll see “Hello World” in your browser. Congratulations, times two! 🏆🏆

I hope you can see how quick it can be to set up an Eleventy project, including integrations with Nunjucks for templating, Tailwind for styles and Alpine.js for scripts. I know working with new tech can be overwhelming and even confusing, so feel free to email me at csstricks@gregwolanski.com if you have problems starting up or have an idea for how to simplify this even further.

The post An Eleventy Starter with Tailwind CSS and Alpine.js appeared first on CSS-Tricks.


, , ,

Learn Eleventy From Scratch

The latest edition of Andy Bell’s Piccalilli landed in my inbox this morning with a sweet offer: preorder Andy’s course on learning Eleventy from scratch at a third of the price.

Why the plug? No, not sponsorships or anything like that. I just happen to hear a heckuva lot about Eleventy these days. Like how we can use it with Google Sheets as a pseudo-CMS. Or how it can be a key component of an emergency website kit. I mean, geez, Chris even used it for the conferences site we have around here. As Andy says, “the future is bright because the future is static.” At least, it certainly appears that way.

I’m squarely in the novice camp when it comes to Eleventy, not to mention static site generators as a whole. That’s why I signed up for the course. It promises to be a deep dive that starts with an empty directory and goes all the way to full-blown website. Given that Andy has created more Eleventy sites than most folks (seriously, it’s documented) and that his Eleventy-powered Piccalilli site notches perfect Lighthouse scores, I think he’ll have a lot to offer in a course.

While we’re on the topic of Eleventy, there are other guides, tutorials and courses out there you might find compelling:

Direct Link to ArticlePermalink

The post Learn Eleventy From Scratch appeared first on CSS-Tricks.


, , ,

Creating an Editable Site with Google Sheets and Eleventy

Remember Tabletop.js? We just covered it a little bit ago in this same exact context: building editable websites. It’s a tool that turns a Google Sheet into an API, that you as a developer can hit for data when building a website. In that last article, we used that API on the client side, meaning JavaScript needed to run on every single page view, hit that URL for the data, and build the page. That might be OK in some circumstances, but let’s do it one better. Let’s hit the API during the build step so that the content is built into the HTML directly. This will be far faster and more resilient.

The situation

As a developer, you might have had to work with clients who keep bugging you with unending revisions on content, sometimes, even after months of building the site. That can be frustrating as it keeps pulling you back, preventing you from doing more productive work.

We’re going to give them the keys to updating content themselves using a tool they are probably already familiar with: Google Sheets.

A new tool

In the last article, we introduced the concept of using Google Sheets with Tabletop.js. Now let’s introduce a new tool to this party: Eleventy

We’ll be using Eleventy (a static site generator) because we want the site to be rendered as a pure static site without having to ship all of the under workings of the site in the client side JavaScript. We’ll be pulling the content from the API at build time and having Eleventy create a minified index.html that we’ll push to the server for the production website. By being static, this allows the page to load faster and is better for security reasons.

The spreadsheet

We’ll be using a demo I built, with its repo and Google Sheet to demonstrate how to replicate something similar in your own projects. First, we’ll need a Google Sheet which will be our data store.

Open a new spreadsheet and enter your own values in the columns just like mine. The first cell of each column is the reference that’ll be used later in our JavaScript, and the second cell is the actual content that gets displayed.

In the first column, “header” is the reference name and “Please edit me!” is the actual content in the first column.

Next up, we’ll publish the data to the web by clicking on File → Publish to the web in the menu bar.

A link will be provided, but it’s technically useless to us, so we can ignore it. The important thing is that the spreadsheet(and its data) is now publicly accessible so we can fetch it for our app.

Take note that we’ll need the unique ID of the sheet from its URL  as we go on.

Node is required to continue, so be sure that’s installed. If you want to cut through the process of installing all of thedependencies for this work, you can fork or download my repo and run:

npm install

Run this command next — I’ll explain why it’s important in a bit:

npm run seed

Then to run it locally:

npm run dev

Alright, let’s go into src/site/_data/prod/sheet.js. This is where we’re going to pull in data from the GoogleSheet, then turn it into an object we can easily use, and finally convert the JavaScript object back to JSON format. The JSON is stored locally for development so we don’t need to hit the API every time.

Here’s the code we want in there. Again, be sure to change the variable sheetID to the unique ID of your own sheet.

 module.exports = () => {   return new Promise((resolve, reject) => {     console.log(`Requesting content from $ {googleSheetUrl}`);     axios.get(googleSheetUrl)       .then(response => {         // massage the data from the Google Sheets API into         // a shape that will more convenient for us in our SSG.         var data = {           "content": []         };         response.data.feed.entry.forEach(item => {           data.content.push({             "header": item.gsx$ header.$ t,             "header2": item.gsx$ header2.$ t,             "body": item.gsx$ body.$ t,             "body2": item.gsx$ body2.$ t,             "body3":  item.gsx$ body3.$ t,             "body4": item.gsx$ body4.$ t,             "body5": item.gsx$ body5.$ t,             "body6":  item.gsx$ body6.$ t,             "body7": item.gsx$ body7.$ t,             "body8": item.gsx$ body8.$ t,             "body9":  item.gsx$ body9.$ t,             "body10": item.gsx$ body10.$ t,             "body11": item.gsx$ body11.$ t,             "body12":  item.gsx$ body12.$ t,             "body13": item.gsx$ body13.$ t,             "body14": item.gsx$ body14.$ t,             "body15":  item.gsx$ body15.$ t,             "body16": item.gsx$ body16.$ t,             "body17": item.gsx$ body17.$ t,                        })         });         // stash the data locally for developing without         // needing to hit the API each time.         seed(JSON.stringify(data), `$ {__dirname}/../dev/sheet.json`);         // resolve the promise and return the data         resolve(data);       })       // uh-oh. Handle any errrors we might encounter       .catch(error => {         console.log('Error :', error);         reject(error);       });   }) }

In module.exports, there’s a promise that’ll resolve our data or throw errors when necessary. You’ll notice that I’m using a axios to fetch the data from the spreadsheet. I like the it handles status error codes by rejecting the promise automatically, unlike something like Fetch that requires monitoring error codes manually.

I created a data object in there with a content array in it. Feel free to change the structure of the object, depending on what the spreadsheet looks like.

We’re using the forEach() method to loop through each spreadsheet column while equating it with the corresponding name we want to allocate to it, while pushing all of these into the data object as content. 

Remember that seed command from earlier? We’re using seed to transform what’s in the data object to JSON by way of JSON.stringify, which is then sent to src/site/_data/dev/sheet.json

Yes! Now have data in a format we can use with any templating engine, like Nunjucks, to manipulate it. But, we’re focusing on content in this project, so we’ll be using the index.md template format to communicate the data stored in the project.

For example, here’s how it looks to pull item.header through a for loop statement:

<div class="listing"> {%- for item in sheet.content -%}   <h1>{{ item.header }} </h1> {%- endfor -%} </div>

If you’re using Nunjucks, or any other templating engine, you’ll have to pull the data accordingly.

Finally, let’s build this out:

npm run build

Note that you’ll want a dist folder in the project where the build process can send the compiled assets.

But that’s not all! If we were to edit the Google Sheet, we won’t see anything update on our site. That’s where Zapier comes in. We can “zap” Google sheet and Netlify so that an update to the Google Sheet triggers a deployment from Netlify.

Assuming you have a Zapier account up and running, we can create the zap by granting permissions for Google and Netlify to talk to one another, then adding triggers.

The recipe we’re looking for? We’re connecting Google Sheets to Netlify so that when a “new or updated sheet row” takes place, Netlify starts a deploy. It’s truly a set-it-and-forget-it sort of deal.

Yay, there we go! We have a performant static site that takes its data from Google Sheets and deploys automatically when updates are made to the sheet.

The post Creating an Editable Site with Google Sheets and Eleventy appeared first on CSS-Tricks.


, , , , ,

Eleventy Love

Been seeing a lot of Eleventy action lately. It’s a smaller player in the world of static site generators, but I think it’s got huge potential because of how simple it is, yet does about anything you’d need it to do. It’s Just JavaScript™.

The post Eleventy Love appeared first on CSS-Tricks.