Build a static website with Eleventy and Tailwind CSS on Fedora Silverblue 43

Let's build a static website using Podman, Eleventy, and Tailwind CSS on Fedora Silverblue.

Build a static website with Eleventy and Tailwind CSS on Fedora Silverblue 43
A concrete guide on how to build an 11ty website with Tailwind on Fedora Silverblue.

I recently had to build an informational website, so I thought a static site would be the way to go. I created a HTML page, embedded the Tailwind CSS Play CDN script, put together a few Tailwind blocks, and filled them with copy. Then I copied the page for other pages, swapped out the content, and voilà, my static site was done.

Now, if I want to add something to the menu or footer, it's not hard to manually edit a handful of pages, but it's prone to errors. If I had PHP at my disposal, I would create a couple template parts and include them on each page, so naturally, I wondered if there was a static site generator that could, more or less, do just that.

I looked at a number of static site generators with the hope of finding one that made it easy to convert my existing design into a corresponding theme, but when I discovered Eleventy, I didn't see anything about themes at all. As it turns out, that's a good thing, because it means I don't have to learn how to build a custom theme.

The beauty of Eleventy's simplicity (and power) didn't hit me until I started working with it. After a bit of tinkering to get it all to work on Fedora Silverblue, it was a breeze to convert my website. To save you the trouble, here's a guide on how I did that.

Install Podman Compose

Fedora Silverblue already has Podman installed to run containers, but with Podman Compose, we can spin up the environment with just a single command.

Let's install Podman Compose:

rpm-ostree install podman-compose

We'll have to restart Fedora to apply this package.

Create project directory

I like to keep projects in a Projects directory within my home folder. In this guide, we'll use eleventy as the project directory within Projects.

Let's create a Projects and eleventy directory:

mkdir -p ~/Projects/eleventy

Create package.json file

With the help of NPM, we'll install everything we need to run Eleventy with Tailwind CSS inside a Podman container.

Let's create the package.json file:

{
  "name": "eleventy",
  "version": "1.0.0",
  "type": "module",
  "devDependencies": {
    "@11ty/eleventy": "^3.1.2",
    "@tailwindcss/cli":"^4.1.18",
    "tailwindcss": "^4.1.18",
    "npm-run-all": "^4.1.5"
  },
  "scripts": {
    "dev": "npm-run-all --parallel dev:css dev:site",
    "dev:css": "npx @tailwindcss/cli -i ./styles/styles.css -o ./public/styles/styles.css --watch",
    "dev:site": "npx @11ty/eleventy --serve"
  }
}

Our development dependencies are:

  • @11ty/eleventy: To generate our static website.
  • @tailwindcss/cli: To generate our Tailwind styles.
  • tailwindcss: To style our website with Tailwind.
  • npm-run-all: To run two watchers in parallel (see below).

And then we create a few scripts to:

  • dev: Watch for both CSS and site changes.
  • dev:css: Watch for CSS changes and output styles.css.
  • dev:site: Watch for site changes and serve the site.

Create docker-compose.yml file

Docker and Podman composition files are interchangeable, hence the name docker-compose.yml. This will build a simple container to generate and serve our site locally.

Let's create the docker-compose.yml file:

version: "3.8"

services:
  web:
    image: node:24-alpine
    working_dir: /app
    volumes:
      - ./:/app:Z
    ports:
      - "8011:8080"
    command: sh -c "npm install && npm run dev"

Here's what it does:

  • web: Creates a container called web.
  • image: Uses the Node (version 24) Alpine image for the container.
  • working_dir: Runs commands from that directory.
  • volumes: Mounts the eleventy directory to the app directory inside the container.
  • ports: Exposes port 8080 within the container to us locally on port 8011.
  • command: Runs npm install and npm run dev when the container starts.

Define project structure

We can set up the project in many different ways, but here's one way to do it. Once you see how it all comes together, you can change this structure (and update necessary configuration files) as needed.

eleventy/
|– content/
|– favicon/
|– images/
|– public/
|– scripts/
|– styles/
|– .gitignore
|– docker-compose.yml
|– eleventy.config.js
|– package.json

Let's walk through each directory/file:

  • eleventy is the project directory.
  • content is for pages, layouts, etc.
  • favicon is for favicon files (if you have them).
  • images is for website images (if you have some).
  • public is automatically created by Eleventy when the site is generated.
  • scripts is for custom JavaScript (if you have any).
  • styles is for us to import Tailwind into the project.
  • .gitignore contains node_modules so that this directory doesn't get committed.
  • docker-compose.yml contains instructions on how to build the container.
  • eleventy.config.js tells Eleventy how the project is structured (more on that below).
  • package.json contains the packages required to build and run the website.

Create eleventy.config.js file

With the project structure in hand, you can now cross-reference it with the Eleventy configuration file below and make changes as you see fit.

Let's create the eleventy.config.js configuration file:

export default async function(eleventyConfig) {
  eleventyConfig.addPassthroughCopy("favicon");
  eleventyConfig.addPassthroughCopy("images");
  eleventyConfig.addPassthroughCopy("scripts");

  eleventyConfig.setInputDirectory("content");
  eleventyConfig.setOutputDirectory("public");
};
  • addPassthroughCopy: Means that the favicon, images, and script directories are copied as-is from the eleventy directory into the public directory, which will contain the statically generated website. If you have none of those resources, you don't have to create or copy those directories.
  • setInputDirectory: Configures the directory Eleventy will watch for page and layout changes to generate the website. If not configured, the default would be the project root.
  • setOutputDirectory: Configures the directory Eleventy will output the generated website in and serve it from there. If not configured, the default would be _site.

Create styles.css for Tailwind

In order to use Tailwind CSS, we need to import the package into our project.

Let's create a styles.css file within the styles directory:

@import "tailwindcss";

If you need to set any theme variables, you can do so right after @import.

Create a simple website

With all of the infrastructure in place, we can build a simple website with a single layout and a couple pages. You can use this setup to then add additional pages and/or layouts as required.

Let's create a default.liquid layout file in the content/_includes/layouts directory:

---
title: Example Industries
description: A brief description on what we do.
image: https://example.org/images/background.jpg
---
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  
  <title>{% if title != 'Example Industries' %}{{ title }} | Example Industries{% else %}{{ title }}{% endif %}</title>
  <meta name="description" content="{{ description }}" />
  <meta name="generator" content="{{ eleventy.generator }}">

  <meta property="og:url" content="https://example.org/" />
  <meta property="og:type" content="website" />
  <meta property="og:title" content="{{ title }}" />
  <meta property="og:description" content="{{ description }}" />
  <meta property="og:image" content="{{ image }}" />
  
  <meta name="robots" content="index, follow">
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="language" content="English">

  <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
  <link rel="manifest" href="/favicon/manifest.json">

  <link rel="stylesheet" href="/styles/styles.css" />
</head>

<body>
  {{ content }}
  <script src="/scripts/scripts.js"></script>
</body>

</html>

Using front matter (the top part of the file), we can set default data to be used by our layout file for title, description, and image. That is to say, if a specific page using the default layout doesn't provide this data, it will default to what's set in the layout.

We then output this data via {{ title }}, {{ description }}, and {{ image }}. Note that we also have access to what generated the site via {{ eleventy.generator }}. It's not required, but a good way to support the Eleventy project.

The {{ content }} gets replaced with whatever the page content is in a file that implements the default layout.

Let's create a index.liquid file in the content directory to be our Homepage:

---
layout: layouts/default.liquid
---
<h1 class="text-3xl font-bold underline">
  {{ title }}
</h1>
<p>Welcome to the homepage.</p>

Using layout, we can tell Eleventy which layout file to use. It's also important to note how front matter data cascades, because it allows us to both reuse the value of title as defined in the layout file with {{ title }}, or we could override it by adding a title below layout.

In the example above, {{ title }} will render "Example Industries."

Let's also create a about.liquid file in the content directory to be our About page:

---
layout: layouts/default.liquid
title: About
description: Welcome to the about page.
---
<h1 class="text-3xl font-bold underline">
  {{ title }}
</h1>
<p>Welcome to the about page.</p>

In the example above, {{ title }} will render "About | Example Industries" due to the conditional in the layout file.

While this guide uses Liquid templates, you can choose from a number of templating languages– or mix and match them.

Generate static site files

Start the container to generate the site, watch for future CSS and site changes, and serve the site for us to see:

podman-compose up

You can visit your website here: http://localhost:8011.

It's serving everything contained in the public directory, and if you look closely, you'll see something amazing. Your index.liquid has become the root of your site at http://localhost:8011/, but your about.liquid isn't at http://localhost:8011/about.html, it's at http://localhost:8011/about/.

Eleventy is quite intuitive and powerful on how it generates its permalinks.

This means that if you just have a handful of pages, you can keep them all in the root of content, but if you need something more segregated or complicated, you can also opt to create sub directories. I really love how they solved this.

As you add pages or change styles, the site will automatically regenerate and live reload in your browser. When you're done, you can hit CTRL + C to stop the container. When you're ready to publish your site, take everything within the public directory and upload it to any static website host.

In closing, Eleventy surprised me and made it quite simple to convert a truly static site to something more dynamic. I recommend you take a look at the Eleventy docs to see what else it can do. Good luck with your project!

Featured image by Marcel Eberle.